From f3b52b737f8ee9b6fb88a7f4bc941ab7a22e15e9 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 12:46:33 -0300 Subject: [PATCH 01/42] spec: define nominal tagged unions --- .../changes/add-nominal-unions/.openspec.yaml | 2 + openspec/changes/add-nominal-unions/design.md | 389 ++++++++++++++++++ .../changes/add-nominal-unions/proposal.md | 88 ++++ .../specs/bootstrap-analysis-facade/spec.md | 20 + .../specs/bootstrap-backend/spec.md | 23 ++ .../specs/bootstrap-declaration-index/spec.md | 43 ++ .../specs/bootstrap-evaluation/spec.md | 18 + .../bootstrap-exhaustive-matching/spec.md | 73 ++++ .../specs/bootstrap-flow-functions/spec.md | 33 ++ .../specs/bootstrap-hir/spec.md | 19 + .../specs/bootstrap-host-input/spec.md | 27 ++ .../specs/bootstrap-integer-scalars/spec.md | 31 ++ .../bootstrap-intrinsic-boundary/spec.md | 135 ++++++ .../specs/bootstrap-lexer/spec.md | 12 + .../specs/bootstrap-mir/spec.md | 29 ++ .../bootstrap-module-semantic-surface/spec.md | 25 ++ .../specs/bootstrap-name-resolution/spec.md | 37 ++ .../spec.md | 18 + .../bootstrap-nominal-effect-storage/spec.md | 18 + .../specs/bootstrap-nominal-unions/spec.md | 119 ++++++ .../specs/bootstrap-os-file-system/spec.md | 45 ++ .../specs/bootstrap-ownership/spec.md | 35 ++ .../specs/bootstrap-semantic-facts/spec.md | 58 +++ .../specs/bootstrap-silk-stdlib/spec.md | 55 +++ .../specs/bootstrap-standard-input/spec.md | 26 ++ .../specs/bootstrap-structural-unions/spec.md | 46 +++ .../specs/bootstrap-syntax/spec.md | 30 ++ .../specs/bootstrap-target-layout/spec.md | 68 +++ .../specs/bootstrap-type-generics/spec.md | 40 ++ .../specs/silk-source-formatting/spec.md | 20 + openspec/changes/add-nominal-unions/tasks.md | 101 +++++ 31 files changed, 1683 insertions(+) create mode 100644 openspec/changes/add-nominal-unions/.openspec.yaml create mode 100644 openspec/changes/add-nominal-unions/design.md create mode 100644 openspec/changes/add-nominal-unions/proposal.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-backend/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-declaration-index/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-evaluation/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-exhaustive-matching/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-host-input/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-integer-scalars/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-mir/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-module-semantic-surface/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-name-resolution/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-nominal-callable-storage/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-nominal-effect-storage/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-nominal-unions/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-os-file-system/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-ownership/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-semantic-facts/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-standard-input/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-structural-unions/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-syntax/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/bootstrap-type-generics/spec.md create mode 100644 openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md create mode 100644 openspec/changes/add-nominal-unions/tasks.md diff --git a/openspec/changes/add-nominal-unions/.openspec.yaml b/openspec/changes/add-nominal-unions/.openspec.yaml new file mode 100644 index 000000000..7f2cf9bc0 --- /dev/null +++ b/openspec/changes/add-nominal-unions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-28 diff --git a/openspec/changes/add-nominal-unions/design.md b/openspec/changes/add-nominal-unions/design.md new file mode 100644 index 000000000..d08347abd --- /dev/null +++ b/openspec/changes/add-nominal-unions/design.md @@ -0,0 +1,389 @@ +## Context + +See [proposal.md](proposal.md) for motivation and the delta specs for the complete source contract. +This is a cross-cutting compiler change: today, `Type.Nominal` and declaration facts model structs, +scalar enums are a separate fieldless declaration kind, and `Type.StructuralUnion` models normalized +open alternatives. `Option` and `Result` are currently assembled from wrapper structs and detached +member structs, and several compiler paths manufacture those shapes directly. + +The implementation already has most of the required machinery in separate forms: + +- struct declaration, field, generic-construction, visibility, ownership, represented-field, and + layout pipelines; +- structural-union tag, payload, conversion, calling-shape, and active-cleanup pipelines; and +- scalar-enum closed-member lookup and exhaustive-coverage pipelines. + +The design composes those mechanisms without treating a variant as a type or flattening it into a +structural union. The repository is green-field: the old Option/Result encodings and abstraction- +shaped intrinsic signatures are migration inputs to delete, not compatibility contracts. + +## Goals / Non-Goals + +**Goals:** + +- Represent a declared union as one canonical nominal type with subordinate variant and field facts. +- Share struct field semantics and structural-union representation machinery without sharing their + source identities. +- Preserve enough canonical identity through every phase to verify construction, matching, cleanup, + layout, and backend behavior. +- Keep standard-library `Option` and `Result` ordinary source declarations and remove compiler + recognition of their names or old representation shapes. +- Keep malformed declarations navigable while ensuring no partial union becomes executable. + +**Non-Goals:** + +- A public or stable ABI, serialization format, tag value, or representation annotation. +- Raw/C unions, external linkage, tuple variants, variant-local generics, explicit discriminants, or + automatic nominal-union Copy derivation. +- Variant subtyping, direct whole-union field projection, common-field synthesis, or implicit pattern + generic inference. +- Compatibility aliases or parallel old/new Option and Result paths. + +## Decisions + +### 1. The parent is a nominal type; variants are subordinate identities, not types + +`Type.Nominal` remains the value-level identity for a complete union application. Its canonical +declaration kind distinguishes a struct, scalar enum, or nominal union. No `VariantType`, detached +nominal, or structural member is created. + +Declaration facts add three related identities: + +- `UnionFact`, keyed by the ordinary canonical declaration identity; +- `UnionVariantId`, keyed by parent declaration plus variant name, with source ordinal as metadata; +- variant field identity, keyed by variant identity plus field name. + +Field algorithms should accept an aggregate-field owner discriminated as either a struct or a union +variant. This preserves one implementation of field uniqueness, visibility, type resolution, +generic substitution, represented storage, and diagnostics while preventing a union field from being +mistaken for a directly projectable parent field. + +The variant's source ordinal determines its private tag and cleanup/layout ordering. Its canonical +identity uses parent plus name, so reordering changes the surface and representation plan without +pretending that a same-named variant became a different declaration. + +Alternative rejected: synthesize one hidden struct per variant and define the parent as a structural +union. That reproduces the detached identity problem, makes generic parent selection conventional, +and permits normalization to erase variant boundaries. + +### 2. Syntax has dedicated union and variant nodes with two-stage constructor resolution + +The lexer adds one complete-identifier `union` token. The CST adds a union declaration node, unit and +named-field variant nodes, and a dedicated parent-qualified variant selector used by constructors and +patterns. Named-field declaration bodies must contain at least one field; `{}` is rejected in favor +of the unit spelling. + +A constructor is resolved in two stages: + +1. Resolve the qualifier through ordinary module scope to a union declaration and bind a contiguous + explicit parent-argument prefix. +2. Resolve the variant within that declaration, elaborate its supplied fields, and use only those + field constraints to complete the parent application. + +This permits `Option.Some { value: 42 }` and `Result.Failure { error }`, but not +`Result.Success { value: 42 }` when `E` has no field evidence. Unit constructors must provide the +complete parent application. Patterns also require a complete application, so +`Option.Some { value }` is valid while `Option.Some { value }` does not infer from the scrutinee. + +Parser recovery stays within the current variant. The declaration index retains valid siblings, but +any invalid variant or field makes the parent unavailable for construction, coverage, layout, HIR, +MIR, and execution. + +Alternative rejected: infer pattern arguments from the scrutinee or constructor arguments from an +expected result. That would create a second generic-inference policy and contradict existing struct +and call inference boundaries. + +### 3. Unions participate in the ordinary declaration and module-surface graph + +Header collection indexes unions beside functions, structs, enums, services, interfaces, and other +top-level declarations under the existing cross-kind duplicate policy. It collects parent parameters, +variants, and fields before body analysis, then resolves every field type against the completed +closure-wide scope. The recursive layout dependency graph gains edges from each variant field to its +referenced nominal aggregates. + +`ModuleSurface` receives an explicit union record containing declaration kind, visibility, ordered +parameters, ordered variants and kinds, ordered fields, field visibility and types, bounds, and +availability. It excludes source spans and target representation. Changing variant order or any +observable payload contract therefore invalidates dependents; changing a factory body does not. + +Analysis and semantic-occurrence projections expose parent, variant, and field facts directly. +Tooling never reconstructs variant ownership from syntax or private numeric tags. + +### 4. Variant construction reuses one aggregate-field elaborator + +The current struct-literal implementation should be split into a shared aggregate-field elaborator +and thin struct/variant entry points. The shared component owns: + +- source-order initializer retention and declaration-order mapping; +- duplicate, missing, unknown, and inaccessible field diagnostics; +- construction authority and non-disclosing private-field fences; +- initializer compatibility and represented-field realization; and +- explicit generic prefixes, field-only suffix inference, conflicts, and completed substitution. + +The variant entry point supplies the selected variant's field owner and returns a precise applied +parent union. A field variant requires every declared field exactly once. A unit variant bypasses +field elaboration and is allocation-free. + +A parent union has no aggregate field table for expression projection. `value.field` is rejected even +if every variant has an identically named field. Pattern selection is the only safe source operation +that exposes variant fields; its bindings then use ordinary place, borrow, move, write, and cleanup +rules. + +Alternative rejected: synthesize common fields across variants. It complicates mutation and active- +payload proof, gives same-spelled fields accidental semantic coupling, and is not struct parity. + +### 5. Generic specialization substitutes the parent once and never renormalizes variants + +Every variant field refers to parameters owned by the parent union. A complete application is still a +canonical `Type.Nominal` keyed by declaration and ordered arguments. Substitution produces one +ordered specialized variant table for semantic checking, ownership, layout, and lowering. + +Unlike `A | B`, that table never deduplicates or flattens. Equal payload layouts, equal field types, +or an uninhabited field do not erase a variant. A `never` field receives the existing zero-sized, +unmaterializable layout fact: the variant keeps its canonical tag and coverage leaf but cannot be +constructed without a valid `never` value. Structural unions inside fields continue to normalize +after substitution. + +This model also makes `Option` and `Option` distinct roots when both occur in one +structural union. + +### 6. Coverage is a set of canonical selection paths + +Replace the flat match-member coverage key with a canonical selection path: + +```text +SelectionPath = structural root + | structural root -> applied nominal parent -> variant +``` + +For a precise nominal-union scrutinee, the parent itself is the root. For a structural union, each +ordinary member is a root; a nominal-union member expands only in the coverage domain to one leaf per +variant. This expansion never changes `Type.StructuralUnion.members`. + +Coverage transitions are: + +- an unguarded variant pattern removes one leaf; +- an unguarded whole-parent pattern removes its root and all remaining descendant leaves; +- an ordinary exact-member pattern removes its root; +- `_` removes everything; and +- a guarded arm removes nothing. + +Diagnostics render fully qualified paths, including applied generic arguments. A guarded affine +variant arm uses the existing provisional arm-binding model: tag tests and guard evaluation do not +commit field moves or cleanup until the guard succeeds, so a false guard leaves the complete value +available to later arms. + +HIR retains both the outer structural-member selection and inner variant selection. MIR lowers a +direct variant arm to an outer structural tag test when needed, followed by the nominal tag test. +The verifier rejects a payload projection not dominated by the matching variant decision. + +Alternative rejected: require a nested match after selecting the whole parent. It preserves type +identity but adds ceremony and discards the user-approved direct hierarchical matching behavior. + +### 7. Ownership is parent-nominal with active-variant cleanup + +Interface, operator, Copy, and Drop lookup remains keyed by the applied parent nominal type. A union +is affine unless a valid explicit `impl Copy` applies. Copy validation traverses every variant field +under the declared bounds; it cannot infer conformance from currently reachable fields. + +Cleanup planning introduces a nominal-union branch containing one private tag decision and one field +cleanup sequence per variant. It reuses the existing nominal Drop ordering, but traverses only the +active variant's initialized fields. A moved pattern transfers selected fields and retains omitted +fields in that variant's branch-local cleanup. Whole-value movement, structural-union injection, +typed failure transfer, and scope exit preserve one active obligation. Fatal traps keep the existing +no-unwind rule. + +Represented callable and Effect fields use their existing concrete realization and storage fences. +Only the active variant owns their captures or environment; an unrealizable field makes the complete +parent application unavailable before MIR. + +### 8. Layout has a distinct nominal-union plan built from aggregate payload plans + +Internal names must distinguish nominal and structural unions. Use tags such as +`NominalUnionRepresentation`, `NominalUnionCallingShape`, and `NominalUnionCleanup` rather than +overloading existing `Union` records whose meaning is structural. + +For each concrete parent application, target planning builds: + +```text +NominalUnionLayout + parent + private tag representation + payload offset, size, alignment + total size, alignment, padding + variants[] + variant identity, source ordinal, private tag + aggregate payload layout + logical-field-to-fixed-slot calling mapping +``` + +Each field variant's payload uses the existing declaration-ordered struct field offset and padding +algorithm, including concrete callable and Effect realizations. The enclosing payload uses the +maximum variant size and alignment. The private tag uses the existing deterministic private-tag +width policy and source-order ordinal; no source or external ABI observes it. + +Complete non-generic unions enter the nominal layout catalog before runtime reachability, including +unavailable and unused private declarations. Open generics get no speculative layout. Reachable +concrete generic applications receive canonical specialized entries. Mixed struct/union recursion is +checked in one inline dependency graph, with explicit existing indirection as the only cycle break. + +The calling shape is a tag lane plus fixed payload slots and a complete mapping from every variant's +logical aggregate lanes. MIR, evaluation, Wasm, and LLVM consume this one plan; backends do not infer +offsets, tag order, or call ABI independently. + +### 9. HIR and MIR use explicit nominal-union operations + +HIR adds explicit nodes for construction and variant selection. A construction records the applied +parent, variant, specialized declaration-ordered field initializers, source mapping, access facts, and +precise result. A pattern records its complete selection path, bindings, omissions, access mode, +guard, and active cleanup branch. + +MIR adds monomorphic operations for: + +- constructing a nominal union from one verified variant payload; +- testing/selecting a variant through a verified layout; +- projecting a selected payload field with its variant identity; and +- dispatching active-variant copy/drop behavior. + +The MIR verifier checks parent application, variant ownership, field completeness and types, +selection dominance, layout/calling-shape identity, hierarchical coverage completeness, and cleanup +branch correctness. Canonical encoding orders applications, variants, fields, paths, and cleanup by +their canonical keys rather than traversal order. + +The evaluator stores semantic parent, variant, and payload identities directly. Wasm and LLVM lower +the verified private tag and payload plan. This keeps evaluator traces readable without making the +numeric tag source-observable. + +### 10. Existing recoverable intrinsics become carrier-neutral + +The old compiler paths manufacture `Option` and `Result` by spelling and representation shape. They +must be removed with the wrapper encodings. The intrinsic inventory is reshaped without adding a new +source-callable operation. + +Checked scalar primitives become generic over an ordinary result carrier `B` and accept two exact +`once fn` constructors conceptually equivalent to: + +```text +checked(operands, present: once fn(T) -> B, absent: once fn() -> B) -> B +``` + +The selected callback is invoked exactly once; the unused callable environment is cleaned normally. +Integer wrappers pass `some` and `none`, so public operations still return `Option`, while an +equivalent user wrapper may choose another carrier without compiler registration. + +Completed Effect reification similarly becomes a carrier-neutral fold: + +```text +effectOutcome( + protected: once Effect, + success: once fn(A) -> B, + failure: once fn(E) -> B, +) -> B ? R +``` + +`Effect.result` passes ordinary `succeed` and `failResult` functions. The primitive +preserves lazy timing, access, ownership, cleanup, requirements, and future suspension, but contains +no Result identity. + +Unsafe host primitives that only report counts use a `bool` result plus explicit initialized +count/reason/code outputs. Handle-producing file and directory opens cannot use an optional handle +output because `OsHandle` is affine and a failed call cannot initialize it. Those opens instead take +an exact `once fn(OsHandle) -> B` success carrier and `once fn() -> B` failure carrier while retaining +initialized scalar reason/code outputs. Success transfers the new handle and its close obligation only +to the selected callback; failure creates no handle. Ordinary source then constructs Option or domain +data. This removes Option from low-level OS, standard-input, child-process, and process-input contracts +without adding partial initialization semantics. + +Alternative rejected: resolve canonical `silk.option.Option` or `silk.result.Result` inside compiler +phases. Even if the lookup used a declaration index, the compiler would still grant library identity +by module/name spelling and would retain the abstraction-shaped privilege this migration is meant to +remove. + +### 11. Option and Result migrate atomically after compiler support is complete + +Canonical source becomes conceptually: + +```silk +pub union Option { + None, + Some { pub value: T }, +} + +pub union Result { + Success { pub value: A }, + Failure { pub error: E }, +} +``` + +The public constructor helpers remain ordinary ergonomic functions because unit variants and +parent-only parameters often require explicit arguments. They return direct variants and add no +representation layer. All combinators, integer wrappers, Effect wrappers, filesystem/process code, +fixtures, examples, doctests, and reference pages migrate in the same change. Detached member +imports, wrapper-field matches, old Type helpers, and backend special cases are deleted. + +No intermediate source revision with both representations is a supported endpoint. The implementation +may be developed in compiler-first commits, but the completed change admits only the direct nominal +definitions. + +### 12. Diagnostics and verification follow existing evidence tiers + +Add stable structured diagnostics for empty unions/variants, duplicate variants, invalid variant +qualifiers, incomplete parent applications, foreign variants, private construction fences, parent +field projection, incomplete hierarchical paths, and invalid/unavailable parent applications. +Reuse existing struct field, generic inference, ownership, visibility, represented-storage, and inline +recursion diagnostics when their payload already expresses the exact cause. + +Tests prove each claim at the cheapest layer: + +- lexer/parser/formatter and recovery tests for syntax; +- declaration, module-surface, semantic-fact, inference, visibility, projection, and coverage tests + through `Analysis`; +- evaluator tests for language semantics and cleanup; +- Wasm tests only for representation/codegen claims; +- native-only cases through the shared differential acceptance corpus; and +- deterministic MIR/layout encodings through committed in-process goldens, with fresh-process + coverage left to the repository's global determinism canaries. + +## Risks / Trade-offs + +- **[Risk] `union` collides conceptually and internally with structural unions.** → Keep + `NominalUnion` in compiler identifiers and documentation wherever ambiguity exists; reserve plain + `Union` internally for the established structural representation. +- **[Risk] Refactoring the large struct-literal analyzer creates behavioral drift.** → Extract the + aggregate-field engine under existing struct tests before adding the variant entry point; require + byte-identical existing struct facts and diagnostics. +- **[Risk] Hierarchical matching moves affine fields before a guard commits.** → Represent selection + paths independently of bindings and retain the current provisional guard transaction; add a false- + guard affine-payload cleanup test. +- **[Risk] Nested structural and nominal tags are flattened accidentally during optimization.** → + Keep both identities in HIR/MIR and make verifier dominance/layout checks reject a flattened path. +- **[Risk] Uninhabited payloads produce inconsistent coverage and layout.** → Preserve the declared + leaf and tag, use the existing zero-sized unmaterializable `never` entry, and require exhaustive + coverage without permitting construction. +- **[Risk] Carrier-neutral intrinsics increase callable and cleanup pressure.** → Admit only exact + static `once fn` carriers, reuse represented-callable realization, verify unused-carrier cleanup, + and keep the operation inventory count unchanged. +- **[Risk] The Option/Result rewrite touches a large source corpus.** → Land it only after compiler + parity is available, migrate with repository-wide searches and generated-manifest refreshes, and + reject all stale detached declarations/imports in an explicit removal test. + +## Migration Plan + +1. Add syntax, declaration identities, field-owner generalization, module surfaces, semantic facts, + diagnostics, and formatter support while keeping invalid parents non-executable. +2. Add constructor elaboration, generic completion, projection rejection, variant patterns, canonical + selection paths, and hierarchical exhaustiveness. +3. Add ownership, represented-field realization, layout catalog/calling shapes, HIR, MIR, + verification, evaluation, Wasm, and LLVM support. +4. Reshape checked-scalar and Effect-outcome contracts around exact carriers, handle-producing opens + around affine success/failure carriers, and count-producing host operations around primitive status + plus initialized scalar outputs; update canonical source wrappers and remove direct Option/Result + construction from compiler code. +5. Replace `option.silk` and `result.silk`, migrate all source callers/tests/docs/fixtures/manifests, + and delete wrapper structs, detached members, old type helpers, lowering branches, and backend + assumptions. +6. Run focused semantic and engine tests, then the repository-required `pnpm typecheck`, Biome check, + test suite, `pnpm check`, and release-candidate verification when package contents change. + +There is no data or external ABI migration. Rollback is a whole-change source revert; no compatibility +format is retained or emitted. diff --git a/openspec/changes/add-nominal-unions/proposal.md b/openspec/changes/add-nominal-unions/proposal.md new file mode 100644 index 000000000..064f576a4 --- /dev/null +++ b/openspec/changes/add-nominal-unions/proposal.md @@ -0,0 +1,88 @@ +## Why + +Silk's structural unions compose unrelated types well, but they cannot declare one closed nominal +sum whose variants share generic parameters and belong to the same type. Closed types such as +`Option` and `Result` therefore require wrapper structs around conventionally related structural +members, adding an artificial value layer and making variant identity, generic selection, and +exhaustive matching indirect. + +Nominal unions fill that gap without changing the open composition model: `union Result` is +one declared type with `Success` and `Failure` variants, while a payload such as +`HttpErrorCode | OutOfMemoryError` remains an ordinary structural union selected independently by +each function signature. + +## What Changes + +- Add nonempty nominal `union` declarations with source-ordered unit and named-field variants. +- Make variants subordinate to the instantiated parent type. `Result.Success { value }` + explicitly selects a variant of `Result`; named payload fields may infer omitted parent + arguments under the existing struct-construction rules, while expected result types do not. +- Give union fields the same declaration, visibility, generic substitution, construction, pattern, + ownership, and cleanup rules as struct fields. Unions are affine by default and admit `Copy`, + `Drop`, operator, and interface implementations under the same validation rules as structs. +- Treat a nominal union as one atomic member of a structural union. Structural normalization never + flattens or merges its variants, even after generic specialization. +- Extend exhaustive matching with hierarchical coverage. A match over + `HttpErrorCode | OutOfMemoryError` may cover `HttpErrorCode.DNSTimeout`, + `HttpErrorCode.DNSError { ... }`, and `OutOfMemoryError {}` directly; a whole + `HttpErrorCode value` pattern covers the remaining subtree. +- Plan an inaccessible active tag plus the selected variant's aligned payload through the existing + target-neutral layout, ownership, HIR, MIR, evaluation, and backend pipelines. Direct inline + recursion is rejected under the same finite-layout rule as recursive structs; indirection remains + explicit in source. +- **BREAKING**: Replace the standard-library `Option` and `Result` wrapper structs and their detached + structural-union member structs with direct nominal unions. Update every caller, test, fixture, + and document in the same change, and remove the superseded encodings rather than retaining aliases + or compatibility paths. + +## Capabilities + +### New Capabilities + +- `bootstrap-nominal-unions`: closed nominal tagged unions, including declaration and variant + identity, generic construction, field payloads, matching, ownership, layout, and diagnostics. + +### Modified Capabilities + +- `bootstrap-lexer`, `bootstrap-syntax`: reserve `union` and parse recoverable declarations, + instantiated variant selection, construction, and patterns. +- `bootstrap-declaration-index`, `bootstrap-name-resolution`, `bootstrap-type-generics`: collect + canonical union/variant/field facts and resolve variants through a parent application completed + from an explicit generic prefix and constructor fields. +- `bootstrap-module-semantic-surface`, `bootstrap-semantic-facts`: encode union declarations and + variant operations as deterministic cross-module contracts and immutable semantic facts. +- `bootstrap-intrinsic-boundary`, `bootstrap-integer-scalars`: keep checked and host primitives + carrier-neutral while ordinary integer wrappers construct the new nominal `Option` variants. +- `bootstrap-os-file-system`, `bootstrap-standard-input`, `bootstrap-host-input`: replace raw + Option-shaped host outcomes with affine-safe handle carriers or primitive status/count outputs while + ordinary providers retain their public Option-using APIs. +- `bootstrap-structural-unions`, `bootstrap-exhaustive-matching`: retain nominal unions as atomic + structural members while supporting direct hierarchical variant coverage. +- `bootstrap-ownership`, `bootstrap-target-layout`: apply struct-equivalent ownership and finite + recursive-layout rules while planning one inaccessible tag and active payload. +- `bootstrap-nominal-callable-storage`, `bootstrap-nominal-effect-storage`: realize represented + callable and Effect fields inside the selected variant under the existing nominal-storage rules. +- `bootstrap-hir`, `bootstrap-mir`: retain parent union, variant, specialized field, selection, + binding, cleanup, and representation identities through verified lowering. +- `bootstrap-evaluation`, `bootstrap-backend`: construct, pass, project, match, and clean nominal + unions consistently across evaluation, direct Wasm, and native LLVM execution. +- `bootstrap-analysis-facade`: expose immutable union declaration, variant, field, coverage, layout, + and lowering facts to tooling. +- `bootstrap-flow-functions`, `bootstrap-silk-stdlib`: reify Effect outcomes through the direct + nominal `Result` representation, redefine `Option` and `Result` as nominal unions, and remove their + wrapper and detached-member representations. +- `silk-source-formatting`: define canonical formatting for union declarations, unit and field + variants, instantiated variant paths, constructors, and patterns. + +## Impact + +The change crosses the complete language pipeline: tokens and CST nodes, formatting and recovery, +declaration facts and module surfaces, name and generic resolution, construction and pattern +analysis, hierarchical coverage, ownership and cleanup, HIR/MIR, layout verification, evaluation, +both backends, analysis projections, diagnostics, the standard library, reference documentation, +and acceptance tests. + +No compiler-known standard-library actor, source-callable intrinsic, raw union, C layout, external +linkage, tuple variant, public runtime tag, explicit discriminant, or automatic nominal-union `Copy` +derivation is introduced. Scalar `enum` remains the separate fixed-width, fieldless enumeration +construct, and `A | B` remains the open structural-union construct. diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md new file mode 100644 index 000000000..18bb05878 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Nominal union facts are analysis-facade queries + +The analysis facade SHALL expose immutable query results for union declarations, applied parent +types, ordered variants, fields, visibility, generic substitution, constructors, patterns, +hierarchical coverage, ownership, layouts, HIR, MIR, evaluation, and emission provenance. Tooling +MUST consume those facts rather than reconstructing variant relationships or tag behavior from +syntax. + +#### Scenario: Query one union declaration + +- **WHEN** a consumer asks for a generic union and one variant at their source positions +- **THEN** the facade returns canonical parent, parameter, variant, field, validity, and source facts from one coherent snapshot + +#### Scenario: Preserve recovery isolation + +- **WHEN** one variant is damaged beside valid siblings +- **THEN** facade queries expose its unavailable state while retaining navigable facts for the valid variants and unrelated declarations + diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-backend/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-backend/spec.md new file mode 100644 index 000000000..a9cb40720 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-backend/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Backends realize verified nominal union layouts + +Native LLVM and direct WebAssembly backends SHALL consume the compiler-owned nominal-union layout +and MIR decisions to emit construction, transport, tag dispatch, payload access, and active cleanup. +Backends MUST NOT choose variant order, tag identity, payload layout, hierarchical coverage, or +cleanup policy independently and MUST produce behavior equivalent to evaluation. + +#### Scenario: Emit one mixed nominal union + +- **WHEN** verified MIR passes and returns a union containing unit and aligned payload variants +- **THEN** both backends use the planned calling shape and produce the same selected variant and field values as evaluation + +#### Scenario: Dispatch a direct nested variant arm + +- **WHEN** verified MIR matches `HttpError.Dns` through an outer `HttpError | OutOfMemoryError` +- **THEN** both backends realize the complete outer and inner decision path without exposing either numeric tag + +#### Scenario: Release only the active payload + +- **WHEN** a union with distinct cleanup-bearing variants is dropped on each structured cleanup-bearing exit +- **THEN** native and Wasm release exactly the selected variant's fields once, agree with evaluation, and perform no unwind cleanup for a fatal trap diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-declaration-index/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-declaration-index/spec.md new file mode 100644 index 000000000..65f93c13b --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-declaration-index/spec.md @@ -0,0 +1,43 @@ +## ADDED Requirements + +### Requirement: Nominal unions join the canonical declaration index + +The declaration index SHALL publish each union's canonical nominal identity, ordered type +parameters, visibility, ordered variants, variant kind, named fields, field visibility and declared +types, syntax, and explicit validity state before expression bodies are resolved. Variant and field +identities SHALL be subordinate to the canonical parent union rather than detached declarations. +Union declarations SHALL join functions, structs, enums, and other top-level declarations in the +ordinary module namespace and cross-kind duplicate policy. + +#### Scenario: Index a generic union before bodies + +- **WHEN** one module declares `union Result { Success { value: A }, Failure { error: E } }` +- **THEN** later headers and bodies resolve the same canonical parent, variant, field, and parameter identities independent of source traversal order + +#### Scenario: Preserve damaged declaration facts + +- **WHEN** one variant field is unavailable but sibling variants are valid +- **THEN** the index retains explicit unavailable state for the damaged field and queryable canonical facts for the valid siblings + +#### Scenario: Reject a cross-kind union collision + +- **WHEN** a module declares `struct Result {}` and then `union Result { Success }` +- **THEN** the struct retains the canonical module-level identity and the union remains an explicit cross-kind duplicate + +### Requirement: Union field headers resolve before bodies + +Every identified union header SHALL publish ordered variant and field headers and resolve each field +type against completed closure-wide declaration and module scopes before any expression body is +elaborated. Forward and cross-module type paths SHALL use canonical identities; missing, unknown, +inaccessible, conflicting, duplicate, recursive, and visibility-invalid states SHALL remain explicit +without fabricated fallback types. + +#### Scenario: Resolve a forward variant field + +- **WHEN** a variant field names a public nominal type declared later in the same module +- **THEN** its header resolves that later canonical identity without source-order dependence + +#### Scenario: Preserve an inaccessible variant field type + +- **WHEN** a public variant field exposes a private nominal type +- **THEN** its field dependency remains queryable but unavailable with the ordinary exposure diagnostic before body analysis diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-evaluation/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-evaluation/spec.md new file mode 100644 index 000000000..3b0e2d217 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-evaluation/spec.md @@ -0,0 +1,18 @@ +## ADDED Requirements + +### Requirement: Evaluation carries nominal union values by active variant + +Evaluation SHALL construct, move, copy when explicitly admitted, borrow, pass, return, store, match, +and clean nominal unions according to their canonical applied parent, active variant, complete field +payload, and MIR layout plan. It SHALL NOT expose or independently select numeric tags, flatten +nominal variants into structural members, or evaluate inactive payload storage. + +#### Scenario: Evaluate construction and direct nested matching + +- **WHEN** a program injects `HttpError.Dns { ... }` into `HttpError | OutOfMemoryError` and matches the variant directly +- **THEN** evaluation selects the `Dns` arm, binds its exact fields, and preserves both nominal and structural identities in the trace + +#### Scenario: Evaluate active cleanup + +- **WHEN** a droppable generic payload is stored in one variant and the union leaves scope through success or typed failure +- **THEN** evaluation releases exactly that active payload once under the verified cleanup plan diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-exhaustive-matching/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-exhaustive-matching/spec.md new file mode 100644 index 000000000..61eaabfe9 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-exhaustive-matching/spec.md @@ -0,0 +1,73 @@ +## MODIFIED Requirements + +### Requirement: Coverage uses canonical union subtraction + +Match arms SHALL be considered in source order over canonical selection paths rooted in the +scrutinee's normalized member set. An ordinary member contributes one root path. A nominal-union +member contributes one root-parent-variant leaf for every canonical variant, including variants with +uninhabited specialized payloads. An unguarded whole-member arm SHALL remove its root and every +remaining descendant; an unguarded qualified variant arm SHALL remove exactly its leaf. A guarded +arm SHALL NOT remove any path. `_` SHALL cover every remaining path and MUST make every following arm +unreachable. A match SHALL be exhaustive only when no paths remain or an explicit universal arm +covers them. Duplicate, unreachable, guard-after-exhaustive-path, and incomplete matches SHALL be +rejected with fully qualified remaining paths and exact arm spans. Variant leaves SHALL never become +members of the structural-union type. + +#### Scenario: Exhaust a two-member union + +- **WHEN** a match over `Token | End` has unguarded `Token` and `End` arms +- **THEN** coverage reaches the empty set without a universal arm + +#### Scenario: Guard does not prove coverage + +- **WHEN** the only `Token` arm has a guard and the scrutinee is `Token | End` +- **THEN** both `Token` and `End` remain in the final uncovered-member diagnostic + +#### Scenario: Reject an arm after universal coverage + +- **WHEN** `_` is followed by another arm +- **THEN** the following arm is diagnosed as unreachable and contributes no binding or result fact + +#### Scenario: Match variants directly through a structural union + +- **WHEN** a match over `HttpError | OutOfMemoryError` has unguarded arms for every `HttpError` variant and `OutOfMemoryError {}` +- **THEN** coverage is exhaustive without requiring an intermediate whole-`HttpError` arm or nested match + +#### Scenario: Cover the remaining nominal subtree + +- **WHEN** one direct `HttpError.Timeout` arm is followed by `HttpError remaining` +- **THEN** the whole-parent arm binds `remaining` as `HttpError` and covers every other `HttpError` variant + +#### Scenario: Reject a leaf after whole-parent coverage + +- **WHEN** `HttpError remaining` is followed by `HttpError.Dns { ... }` +- **THEN** the later variant arm is unreachable because its parent subtree was already removed + +#### Scenario: Keep a guarded affine variant available + +- **WHEN** a guarded direct variant arm inspects an affine payload and its guard is false before a later arm can select the same path +- **THEN** coverage retains the complete path and ownership retains the tags and payload for the later arm without early movement or cleanup + +#### Scenario: Diagnose a missing generic variant path + +- **WHEN** a match over `Option | Option` omits only `Option.Some` +- **THEN** the incomplete-match diagnostic names that fully applied root-parent-variant path without collapsing either Option application + +## ADDED Requirements + +### Requirement: Variant patterns bind struct-like fields + +A named-field variant pattern SHALL bind, rename, nest, borrow, move, omit with `..`, and validate +fields under the same rules as a nominal struct pattern. A unit variant SHALL bind no fields. Pattern +selection SHALL retain the applied parent type and canonical variant identity without introducing a +variant subtype. + +#### Scenario: Move fields from one selected variant + +- **WHEN** `Result.Success { value }` matches a moved `Result` +- **THEN** `value` receives the specialized `A` payload and cleanup remains restricted to that selected variant + +#### Scenario: Reject an incomplete field pattern + +- **WHEN** a variant pattern omits a declared field without `..` +- **THEN** analysis reports the same missing-field condition as struct destructuring and creates no executable arm diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md new file mode 100644 index 000000000..229cf9ec7 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md @@ -0,0 +1,33 @@ +## MODIFIED Requirements + +### Requirement: Completed Effect outcomes can be reified compositionally + +Canonical ordinary Silk `Effect.result` SHALL execute exactly one Effect layer and reify its +completed typed outcome as direct ordinary nominal `Result` data instead of propagating `E`. +It SHALL construct `Result.Success` or `Result.Failure` without a wrapper field, +detached member, or intermediate structural union. Its implementation MAY wrap the minimum sealed +Effect primitive needed to distinguish a completed success from a typed failure, but the compiler +MUST NOT recognize `Result`, its module, or either variant by spelling. The operation SHALL preserve +`R`, ownership, cleanup, run access, and lazy timing, and its contract SHALL remain valid if execution +can suspend before producing the Result in a future runtime. Traps and future interruption MUST NOT +be converted into typed `E` values. + +#### Scenario: Map both completed branches in library code + +- **WHEN** ordinary Silk code reifies `Effect` and matches its Result with success and failure callbacks +- **THEN** either callback can produce the corresponding transformed channel while `R` remains required + +#### Scenario: Preserve future suspension transparency + +- **WHEN** a future execution suspends before its typed outcome completes +- **THEN** outcome reification waits compositionally and does not expose a pending state as `Result` + +#### Scenario: Reify directly into the nominal Result + +- **WHEN** an Effect completes once with success and once with a typed failure +- **THEN** ordinary source constructs the corresponding direct `Success` and `Failure` variants and every downstream phase observes one nominal Result layer + +#### Scenario: Rename an equivalent source wrapper + +- **WHEN** equivalent ordinary source wraps the same minimal Effect primitive under another legal function name +- **THEN** it can construct and return a user-selected nominal union without compiler registration of that union or its variants diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md new file mode 100644 index 000000000..c2736cc57 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: HIR retains nominal union construction and selection identity + +Typed HIR SHALL represent union construction with the canonical applied parent, selected canonical +variant, ordered specialized field initializers, source provenance, and precise nominal result type. +HIR match selections SHALL retain hierarchical coverage identities, field bindings, omissions, +access mode, and active-variant cleanup without erasing a union to a structural member or numeric tag. + +#### Scenario: Lower one generic variant construction + +- **WHEN** analysis accepts `Result.Success { value: 42 }` +- **THEN** HIR records the applied `Result` identity, `Success` variant identity, specialized `value: i32` field, and nominal `Result` result + +#### Scenario: Retain direct nested coverage + +- **WHEN** a match selects `HttpError.Dns` directly from `HttpError | OutOfMemoryError` +- **THEN** HIR retains both the outer structural member and inner nominal variant selection with exact bindings and cleanup + diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-host-input/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-host-input/spec.md new file mode 100644 index 000000000..74727d7bd --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-host-input/spec.md @@ -0,0 +1,27 @@ +## MODIFIED Requirements + +### Requirement: The native provider reads through unsafe OS primitives + +Canonical standard-library source SHALL define `OsHostInput` as an ordinary provider reading the +process command line, environment block, and working directory through unsafe `Intrinsic` operations +returning `bool` and writing complete length, low-level reason, and native code to explicit initialized +scalar outputs. Success SHALL report the value's complete byte length and copy the prefix that fits, +so an undersized buffer is completed by one exactly sized second pass. A `false` result with the +not-found reason SHALL become ordinary absence through the nominal `Option` declaration; any other +`false` result SHALL become `HostInputError`. No compiler phase MAY construct Option or recognize the +`HostInput`, `OsHostInput`, or operation spellings to select special behavior. + +#### Scenario: Complete a value longer than the provider buffer + +- **WHEN** a value is longer than the buffer the provider first offered +- **THEN** the provider learns its complete length and returns the complete value + +#### Scenario: Reject the native lookups on direct WebAssembly + +- **WHEN** a reachable native host-input lookup is compiled for a direct WebAssembly target +- **THEN** target availability rejects it rather than inventing a process-input import + +#### Scenario: Link only the reachable runtime symbols + +- **WHEN** a native program reads host input and touches no filesystem +- **THEN** the artifact links the host-input runtime symbols and no filesystem runtime symbol diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-integer-scalars/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-integer-scalars/spec.md new file mode 100644 index 000000000..e9265fec8 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-integer-scalars/spec.md @@ -0,0 +1,31 @@ +## MODIFIED Requirements + +### Requirement: Integer operations are homogeneous and explicit + +Ordinary arithmetic SHALL accept one identical integer type and trap on overflow, invalid +division/remainder, or invalid shift counts. Comparisons SHALL return `bool`. Every integer SHALL +expose bitwise operations, shifts, and rotates. Named recoverable checked operations SHALL remain +ordinary standard-library wrappers returning direct nominal `Option` values; their sealed scalar +primitives SHALL report only the low-level present-or-absent outcome through carrier-neutral inputs +and MUST NOT construct or recognize `Option` by spelling. Wrapping and saturating variants SHALL +return `T`. No numeric conversion SHALL be implicit. + +#### Scenario: Trap ordinary byte overflow + +- **WHEN** `u8.add(255, 1)` executes +- **THEN** evaluation, native, and WebAssembly trap at the same operation + +#### Scenario: Recover checked overflow + +- **WHEN** `u8.checkedAdd(255, 1)` executes +- **THEN** it returns `Option.None`, while representable addition returns `Option.Some` + +#### Scenario: Reject mixed arithmetic + +- **WHEN** an expression combines `i32` and `i64` without conversion +- **THEN** analysis rejects it without choosing a wider type + +#### Scenario: Rename a checked wrapper and carrier + +- **WHEN** ordinary source calls the same checked scalar primitive with equivalent present and absent constructors for another nominal union +- **THEN** the primitive reports the same arithmetic outcome without compiler registration of either carrier or variant spelling diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md new file mode 100644 index 000000000..ef7c13cc9 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md @@ -0,0 +1,135 @@ +## ADDED Requirements + +### Requirement: Recoverable primitives are carrier-neutral + +No intrinsic contract SHALL name, construct, match, or recognize source-defined `Option`, `Result`, +or their variants. Existing checked scalar primitives SHALL receive ordinary present and absent +carrier inputs and return their shared result type. Existing completed-Effect reification SHALL +receive ordinary success and failure carrier functions and return their shared result type while +preserving its requirement row. The inventory, semantic analysis, HIR, MIR, evaluation, and every +backend SHALL treat those carriers through their ordinary exact callable and value contracts. This +change SHALL replace the abstraction-shaped existing signatures and SHALL add no new source-callable +intrinsic operation. + +#### Scenario: Construct Option in an integer wrapper + +- **WHEN** an ordinary checked-integer wrapper supplies the ordinary `some` and `none` constructor functions to its scalar primitive +- **THEN** the primitive selects the correct ordinary carrier and contains no canonical Option or variant identity + +#### Scenario: Construct Result in Effect.result + +- **WHEN** ordinary `Effect.result` supplies the ordinary `succeed` and `failResult` constructor functions to completed-outcome reification +- **THEN** the primitive invokes exactly one carrier and contains no canonical Result or variant identity + +#### Scenario: Audit the closed inventory + +- **WHEN** the intrinsic inventory is compared before and after migration +- **THEN** abstraction-shaped Option and Result result contracts are gone, no new callable operation exists, and every changed operation has one carrier-neutral contract + +## MODIFIED Requirements + +### Requirement: OS filesystem privilege is handle-level and sealed + +The `Intrinsic` namespace SHALL contain only the unsafe file open/read/write, directory open/next, +path inspection, directory creation, file removal, directory removal, and generic consuming close +operations required to build an OS provider. Their signatures SHALL use primitive scalars, slices, +explicit scalar output parameters, `bool`, exact `once fn` carriers, and opaque `OsHandle`. +Handle-producing opens SHALL transfer a newly initialized handle only as the argument of the selected +success carrier and SHALL select a zero-argument failure carrier after writing reason outputs; they +MUST NOT require an optionally initialized handle place. Count-producing operations SHALL return +`bool` and write counts and failure details to initialized scalar outputs. No operation may use or +construct a source-defined optional carrier, filesystem service, or domain value. + +#### Scenario: Build a source provider from low-level calls + +- **WHEN** canonical `OsFileSystem` implements a whole-file read +- **THEN** it composes open, repeated read, and consuming close rather than invoking a compiler-known whole-file operation + +#### Scenario: Keep portable operations ordinary + +- **WHEN** another source-defined provider implements `FileSystem.readFile` +- **THEN** it can satisfy the service without invoking any OS intrinsic or receiving name-based compiler treatment + +#### Scenario: Transfer one opened handle through a carrier + +- **WHEN** a file or directory open succeeds +- **THEN** the intrinsic invokes the success carrier exactly once with the newly initialized affine `OsHandle`, cleans the unused failure carrier, and transfers one close obligation without an optional output place + +#### Scenario: Refuse an open without initializing a handle + +- **WHEN** a file or directory open fails +- **THEN** the intrinsic writes the normalized reason outputs, invokes the failure carrier exactly once, cleans the unused success carrier, and creates no `OsHandle` or close obligation + +### Requirement: One unsafe byte-input primitive is admitted + +The sealed `Intrinsic` namespace SHALL expose one unsafe native-only byte-input operation taking an +exclusive byte buffer plus exclusive transferred-count, reason, and native-code outputs and returning +`bool`. Success SHALL write the exact transferred byte count, including zero for end of input; failure +SHALL write the normalized low-level reason and native code. The compiler MUST NOT construct or +recognize an optional carrier, `ReadOutcome`, `StreamReadError`, or the `StandardInput` service, and +MUST NOT admit a second input operation for buffering, decoding, or terminal control. + +#### Scenario: Report a refused read + +- **WHEN** the host refuses a standard-input read +- **THEN** the intrinsic returns `false` and writes the normalized reason and native code without constructing a standard-library value + +#### Scenario: Report the end of input + +- **WHEN** the host reports that no further bytes will arrive +- **THEN** the intrinsic returns `true` with a zero count and the library decides what that means + +### Requirement: Two unsafe child-process primitives are admitted + +The sealed `Intrinsic` namespace SHALL expose one unsafe native-only execution operation taking an +executable path, an argument block, an environment block, and a working-directory block as byte +slices plus explicit termination, capture-length, reason, and native-code outputs, and one unsafe +native-only capture operation taking a stream selector, an offset, an exclusive byte buffer, and +exclusive transferred-count and reason outputs and returning `bool`. The argument and environment +blocks SHALL be NUL-terminated entry blocks, and an empty working-directory block SHALL mean the +caller's own directory. A successful execution SHALL retain exactly one capture until the next +execution replaces it. The compiler MUST NOT construct or recognize an optional carrier, +`ProcessRequest`, `ProcessOutcome`, `ProcessError`, or the `ChildProcess` service, and MUST NOT admit +further operations for shells, streaming, or signal delivery. + +#### Scenario: Report a failure to start + +- **WHEN** the host cannot start the requested program +- **THEN** the execution operation reports failure and writes the normalized reason and native code without constructing a standard-library value + +#### Scenario: Report a nonzero exit code as success + +- **WHEN** a child runs to completion and returns a nonzero code +- **THEN** the execution operation succeeds and reports that code as data, leaving the meaning to the library + +#### Scenario: Copy one completed capture + +- **WHEN** a capture reads the retained result of the immediately preceding execution +- **THEN** it returns `true`, commits the requested prefix into the caller's buffer, and writes the exact transferred byte count + +### Requirement: Four unsafe process-input primitives are admitted + +The sealed `Intrinsic` namespace SHALL expose four unsafe native-only process-input operations: an +argument count with an exclusive `usize` output returning `bool`, and argument, environment-value, +and working-directory lookups each taking an exclusive byte buffer plus exclusive complete-length, +reason, and native-code outputs and returning `bool`. Success SHALL write the value's complete byte +length with the prefix that fits copied into the buffer; failure SHALL write the normalized low-level +reason and native code, where the not-found reason means the value does not exist. The compiler MUST +NOT construct or recognize an optional carrier, `HostInputError`, or the `HostInput` service, and MUST +NOT admit an operation that sets an environment variable, changes the working directory, or parses +arguments. + +#### Scenario: Report a value longer than the buffer + +- **WHEN** the host holds a value longer than the buffer the caller supplied +- **THEN** the intrinsic returns `true`, copies the prefix that fits, and writes the complete byte length without a separate buffer-too-small protocol + +#### Scenario: Report an absent value + +- **WHEN** an argument index is past the last argument or an environment name is unset +- **THEN** the intrinsic returns `false` with the not-found reason and the library decides what that means + +#### Scenario: Report a refused lookup + +- **WHEN** the host refuses an otherwise valid process-input lookup +- **THEN** the intrinsic returns `false` with the normalized non-not-found reason and native code diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md new file mode 100644 index 000000000..25525d948 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Union is a complete-identifier keyword + +The lexer SHALL emit `union` as the dedicated nominal-union keyword only when it is a complete +identifier and SHALL retain exact source provenance under the existing trivia and recovery model. + +#### Scenario: Distinguish union from an identifier prefix + +- **WHEN** source contains `union` and `unionize` +- **THEN** the first token is the union keyword and the second remains one identifier token + diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-mir/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-mir/spec.md new file mode 100644 index 000000000..a70a6b9fe --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-mir/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: MIR verifies monomorphic nominal union operations + +MIR SHALL contain only concrete nominal-union applications whose construction, tag selection, +payload fields, moves, projections, matches, and cleanup refer to one verified target-layout entry. +Verification SHALL reject a foreign variant, wrong parent application, duplicate or missing field, +invalid tag decision, payload-layout mismatch, incomplete hierarchical coverage, or cleanup path for +an inactive variant. + +#### Scenario: Verify one concrete constructor + +- **WHEN** lowering emits a `Result.Failure` value +- **THEN** MIR verifies the canonical parent and variant, the specialized `Problem` payload, and the exact planned representation before execution + +#### Scenario: Reject incomplete nested coverage + +- **WHEN** a match plan over `HttpError | OutOfMemoryError` omits one `HttpError` variant without a covering parent or wildcard decision +- **THEN** MIR verification rejects the region rather than allowing a backend default branch + +### Requirement: Nominal union MIR encoding is deterministic + +Equivalent concrete union programs SHALL encode parent, variant, field, hierarchical coverage, +layout, and cleanup identities in canonical order independent of discovery or source-map traversal. + +#### Scenario: Repeat nominal union MIR + +- **WHEN** equivalent generic union facts are lowered under distinct valid discovery traversals +- **THEN** their concrete instance ordering and committed MIR encoding are byte-identical diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-module-semantic-surface/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-module-semantic-surface/spec.md new file mode 100644 index 000000000..b38a2c169 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-module-semantic-surface/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Module surfaces encode nominal union contracts + +A module semantic surface SHALL encode every externally observable nominal-union header fact, +including canonical parent identity, declaration kind, visibility, ordered parameters, ordered +variant identities and kinds, ordered payload fields, field visibility and types, explicit validity, +and generic bounds. It SHALL exclude numeric tags, target layout, bodies, and source spans. Exact +surface equality and dependency invalidation SHALL treat any change to that semantic shape exactly as +an observable nominal struct-shape change. + +#### Scenario: Round-trip a public generic union surface + +- **WHEN** a module exports `Result` with unit or named-field variants +- **THEN** encode and decode preserve the complete ordered parent, parameter, variant, field, visibility, bound, and availability contract + +#### Scenario: Invalidate a dependent after a payload change + +- **WHEN** an exported variant adds, removes, reorders, renames, or changes the type or visibility of a field +- **THEN** the module surface changes and every direct dependent is selected for dependency-surface recomputation + +#### Scenario: Ignore implementation-only edits + +- **WHEN** a factory function body changes without changing the exported union contract +- **THEN** the union portion of the module surface remains equal and does not independently invalidate dependents diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-name-resolution/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-name-resolution/spec.md new file mode 100644 index 000000000..261c6ffa5 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-name-resolution/spec.md @@ -0,0 +1,37 @@ +## ADDED Requirements + +### Requirement: Variants resolve through an instantiated nominal union + +Name resolution SHALL first resolve a constructor qualifier through ordinary module scopes to one +canonical union declaration and bind any contiguous explicit parent-argument prefix. It SHALL then +resolve the variant only within that declaration; named-field inference SHALL complete the parent +application before the canonical selection becomes available. Pattern qualifiers SHALL resolve one +complete applied parent without scrutinee- or expected-type inference. A bare variant name SHALL NOT +search visible union declarations, and a same-spelled variant from another union SHALL remain a +distinct identity. Cross-module access SHALL enforce parent-union and complete-variant construction +authority under the ordinary nominal declaration rules. + +#### Scenario: Resolve one applied variant + +- **WHEN** `Result.Failure` is selected from a visible generic `Result` declaration +- **THEN** resolution records the applied parent arguments and the canonical `Failure` identity owned by `Result` + +#### Scenario: Complete a zero-prefix constructor qualifier + +- **WHEN** `Option.Some { value: 42 }` resolves `Option` and supplies no explicit parent arguments +- **THEN** resolution selects `Some` from the canonical declaration and records the applied `Option` only after field inference completes + +#### Scenario: Reject a variant through the wrong parent + +- **WHEN** two unions declare `Failure` and source selects the first union while requiring the second union's variant +- **THEN** analysis reports the canonical parent mismatch instead of resolving by spelling + +#### Scenario: Keep unqualified variants out of ordinary lookup + +- **WHEN** source refers to `Failure` without an ordinary binding or parent qualifier +- **THEN** resolution reports the ordinary unresolved-name state and does not search union variant sets + +#### Scenario: Refuse pattern inference from the scrutinee + +- **WHEN** a pattern spells `Option.Some { value }` against a scrutinee of type `Option` +- **THEN** resolution reports the incomplete pattern qualifier and requires `Option.Some` diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-callable-storage/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-callable-storage/spec.md new file mode 100644 index 000000000..34570c249 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-callable-storage/spec.md @@ -0,0 +1,18 @@ +## ADDED Requirements + +### Requirement: Active variants store concrete callable representations inline + +A callable-bounded field in a nominal-union variant SHALL use the same finite specialized callable +representation, invocation-access, capture ownership, layout, direct-target lowering, cleanup, and +storage-fence rules as a callable field in a nominal struct. Only the active variant's callable +environment SHALL exist, be invocable after pattern selection, or participate in cleanup. + +#### Scenario: Store and invoke a capturing callable variant + +- **WHEN** a concrete variant stores a capturing section and a borrowing pattern selects that variant +- **THEN** the selected field invokes its static target under the match access mode while inactive variants contribute no callable environment + +#### Scenario: Preserve an unsupported callable fence + +- **WHEN** one reachable variant's callable representation cannot be realized by every required phase and backend +- **THEN** the complete nominal-union application remains unavailable before MIR rather than falling back to a universal callable ABI diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-effect-storage/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-effect-storage/spec.md new file mode 100644 index 000000000..d05d6b923 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-effect-storage/spec.md @@ -0,0 +1,18 @@ +## ADDED Requirements + +### Requirement: Active variants store concrete Effect environments lazily + +An Effect-bounded field in a nominal-union variant SHALL use the same finite specialized runner, +environment, run-access, suspension, ownership, layout, cleanup, and storage-fence rules as an Effect +field in a nominal struct. Construction SHALL remain lazy, and only the active variant's Effect +environment SHALL exist, be runnable after pattern selection, or participate in cleanup. + +#### Scenario: Store and run one selected Effect variant + +- **WHEN** a concrete variant stores an Effect with owned captures and a consuming pattern selects it +- **THEN** construction runs nothing, selection transfers the exact environment once, and execution preserves its success, failure, requirement, access, suspension, and cleanup facts + +#### Scenario: Preserve an unsupported Effect fence + +- **WHEN** one reachable variant's Effect environment cannot be realized by every required phase and backend +- **THEN** the complete nominal-union application remains unavailable before MIR rather than gaining a standalone structural Effect ABI diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-unions/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-unions/spec.md new file mode 100644 index 000000000..82d9cbb76 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-nominal-unions/spec.md @@ -0,0 +1,119 @@ +## Purpose + +Define closed nominal tagged unions whose unit and named-field variants share one declared generic +type while remaining distinct from scalar enums and open structural unions. + +## ADDED Requirements + +### Requirement: A union declares one closed nominal variant set + +`union Name { ... }` SHALL declare one nonempty, source-ordered set of uniquely named variants under +one canonical nominal type. A variant SHALL be either unit or contain one or more named fields; +`Variant {}` SHALL be rejected in favor of the unit spelling `Variant`. Tuple variants, +variant-local generic parameters, discriminants, and representation clauses SHALL NOT be admitted. +The union declaration's visibility SHALL govern its variant names, while payload fields SHALL use +the same default-private visibility, public-exposure, and declaration rules as struct fields. + +#### Scenario: Declare mixed unit and field variants + +- **WHEN** `union HttpError { Timeout, Dns { pub code: u16 } }` is analyzed +- **THEN** `Timeout` and `Dns` are canonical variants of one nominal `HttpError` type and `Dns.code` is a canonical field of that variant + +#### Scenario: Reject duplicate variants + +- **WHEN** one union declares the same variant name twice +- **THEN** analysis reports the later declaration with the first variant's span related and does not invent a second canonical identity + +#### Scenario: Reject an empty union + +- **WHEN** a union declaration contains no variants +- **THEN** analysis reports a deterministic empty-union diagnostic while preserving unrelated declarations + +#### Scenario: Reject an empty named-field variant + +- **WHEN** a union declares `Empty {}` instead of unit variant `Empty` +- **THEN** analysis reports the empty named-field body and retains no second unit-like variant form + +### Requirement: Variant construction selects an instantiated parent union + +A constructor qualifier SHALL resolve a canonical union declaration plus a contiguous explicit +prefix of its parent arguments. Unit selection SHALL require a complete parent application and +construct it directly. Named-field construction MAY complete an omitted argument suffix from its +supplied fields under the ordinary struct-construction inference rules. It SHALL initialize every +declared field exactly once, and every field SHALL be accessible at the construction site; any +inaccessible required field SHALL fence off raw construction for the complete variant. Variant names +SHALL NOT create detached nominal types or unqualified module bindings. + +#### Scenario: Construct an explicitly applied unit variant + +- **WHEN** source constructs `Option.None` +- **THEN** the expression has the precise nominal type `Option` without a payload or allocation + +#### Scenario: Infer a payload argument + +- **WHEN** `Option.Some { value: 42 }` supplies the only field of `Some` and no type argument +- **THEN** construction infers `T = i32` from that field and produces `Option` + +#### Scenario: Keep parent-only parameters explicit + +- **WHEN** `Result.Success { value: 42 }` leaves error parameter `E` absent from every supplied field +- **THEN** construction reports `E` as uninferred even if an expected result type mentions `Result` + +#### Scenario: Fence raw construction with one private field + +- **WHEN** another module selects a public union variant containing one required private field +- **THEN** raw construction is unavailable even when every public field is supplied, while visible factory functions remain callable + +### Requirement: Payload access requires active variant selection + +A value of the parent union type SHALL expose no directly projectable payload field, even when every +variant declares the same field spelling and type. Source SHALL select a variant through a pattern +before it can bind, borrow, move, or write that variant's fields. Failed whole-union projection SHALL +retain the parent and candidate field facts without fabricating a common field identity. + +#### Scenario: Reject projection from the parent value + +- **WHEN** source evaluates `result.value` where `result` has type `Result` +- **THEN** analysis rejects the projection and requires successful variant selection before payload access + +### Requirement: Invalid variants make the parent non-executable + +A duplicate, unidentified, unresolved, visibility-invalid, or otherwise unavailable variant or +field SHALL make the complete applied union unavailable for construction, exhaustive coverage, +layout, HIR, MIR, and execution. The canonical parent identity and independent sibling facts SHALL +remain queryable for parser recovery, diagnostics, navigation, and editing. + +#### Scenario: Preserve siblings without publishing a partial union + +- **WHEN** one payload field type is unresolved beside valid unit and field variants +- **THEN** analysis retains every independent declaration fact but publishes no executable application of the incomplete parent union + +### Requirement: Nominal unions follow nominal struct behavior + +A union SHALL be affine by default and SHALL admit `Copy`, `Drop`, operator, and interface +implementations under the same declaration, bound, coherence, and admissibility rules as a nominal +struct. Generic payload fields SHALL be checked once under declared bounds. Direct inline recursive +storage SHALL be rejected by the same finite-layout rule as structs, while explicit indirection MAY +make recursion finite. + +#### Scenario: Admit conditional Copy + +- **WHEN** `impl Copy for Option` is checked and every variant payload is Copy under that bound +- **THEN** the implementation is accepted for matching concrete applications and the union is not otherwise inferred Copy + +#### Scenario: Reject inline recursive storage + +- **WHEN** a variant field stores its own union type directly with no indirection +- **THEN** layout analysis reports the recursive nominal cycle and publishes no finite layout + +### Requirement: Enum structural-union and nominal-union concepts remain distinct + +A scalar `enum` SHALL remain a fieldless fixed-width enumeration, `A | B` SHALL remain an open +normalized structural union of types, and a declared `union` SHALL remain one closed nominal type +with subordinate variants. No spelling, shape, or specialization SHALL implicitly convert among +those three declaration or type concepts. + +#### Scenario: Preserve three distinct abstractions + +- **WHEN** a program declares a scalar enum, a payload-bearing nominal union, and a structural union containing that nominal union +- **THEN** analysis retains three distinct canonical concepts and never treats a nominal variant as a detached structural member diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-os-file-system/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-os-file-system/spec.md new file mode 100644 index 000000000..2c1913e50 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-os-file-system/spec.md @@ -0,0 +1,45 @@ +## MODIFIED Requirements + +### Requirement: OS intrinsics report low-level outcomes without library values + +Unsafe handle-producing open operations SHALL receive exact success and failure `once fn` carriers. +Success SHALL invoke its carrier with one newly initialized affine `OsHandle`; failure SHALL create no +handle, write a stable low-level numeric reason plus optional native `u32` code to initialized scalar +outputs, and invoke its zero-argument failure carrier. Every other fallible OS operation SHALL return +`bool` and write transferred counts, required capacity, reason, or native code to explicit initialized +scalar outputs as its contract requires. The compiler MUST NOT construct or recognize `Option`, +`Path`, `Bytes`, `DirectoryEntry`, `FileError`, or the portable `FileSystem` service. Read and write +SHALL report transferred byte counts and MAY complete partially. + +#### Scenario: Report a failed open + +- **WHEN** the host refuses a file open +- **THEN** the intrinsic creates no handle, writes the normalized low-level reason and native code, and invokes the failure carrier without constructing a standard-library error + +#### Scenario: Transfer a successful open + +- **WHEN** a file or directory open succeeds +- **THEN** the intrinsic invokes the success carrier exactly once with the new affine handle and transfers one explicit close obligation + +#### Scenario: Report a partial write + +- **WHEN** the host accepts fewer bytes than the supplied slice +- **THEN** the write intrinsic returns `true` and writes the exact positive byte count so ordinary source can continue or translate a later failure + +### Requirement: Directory iteration is retryable and deterministic at the protocol boundary + +Directory-next SHALL return `true` and write `n > 0` for one entry, return `true` and write zero for +end of directory, and return `false` with normalized reason outputs for failure. When the supplied +name buffer is too small, it SHALL report the stable buffer-too-small reason and required capacity +without advancing the iterator. The intrinsic MUST NOT construct an optional carrier, sort entries, +or construct portable paths. + +#### Scenario: Retry an oversized directory name + +- **WHEN** the next entry does not fit the supplied buffer +- **THEN** the call reports the required capacity, leaves the iterator on the same entry, and a sufficiently sized retry returns that entry + +#### Scenario: Reach directory end + +- **WHEN** every host entry has been consumed +- **THEN** directory-next returns `true` with a zero count without fabricating an empty-name entry diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-ownership/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-ownership/spec.md new file mode 100644 index 000000000..f63a88939 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-ownership/spec.md @@ -0,0 +1,35 @@ +## ADDED Requirements + +### Requirement: Nominal union ownership follows nominal struct rules + +A union value SHALL be affine by default. `Copy` and `Drop` implementations, generic Copy bounds, +moves, borrows, writes, partial-move rejection, and implementation admissibility SHALL follow the +same rules as nominal structs across every variant payload. The compiler MUST NOT infer Copy merely +because all currently reachable payload fields are Copy. + +#### Scenario: Require an explicit Copy implementation + +- **WHEN** every field of every variant is Copy but the union declares no valid `impl Copy` +- **THEN** reading the union as a whole consumes it under ordinary affine ownership + +#### Scenario: Validate Copy across every variant + +- **WHEN** a union requests `Copy` and one variant contains an affine field under the declared bounds +- **THEN** conformance is rejected at that field even when another variant is unit + +### Requirement: Cleanup follows exactly one active variant + +Owned union cleanup SHALL run the union's admitted nominal cleanup behavior and recursively clean +exactly the initialized fields of the active variant once. Variant selection, structural-union +injection, moves, borrows, typed-failure transfer, ordinary scope exits, and generic specialization +MUST preserve that single active obligation. Fatal traps SHALL retain the existing no-unwind rule. + +#### Scenario: Clean one selected payload + +- **WHEN** a union holding a droppable field in one variant leaves scope +- **THEN** every engine runs the union-level and active-field cleanup prescribed by ordinary struct ordering without touching inactive variant storage + +#### Scenario: Consume one field variant through matching + +- **WHEN** a moved match extracts one payload field and omits another with `..` +- **THEN** ownership transfers the extracted field once and cleans only the selected variant's omitted fields diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-semantic-facts/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-semantic-facts/spec.md new file mode 100644 index 000000000..6986b3976 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-semantic-facts/spec.md @@ -0,0 +1,58 @@ +## ADDED Requirements + +### Requirement: Semantic facts expose nominal union declarations and variant construction + +Semantic analysis SHALL publish canonical facts for each union parent, parent parameter, source- +ordered variant, variant kind, field, visibility, availability, explicit constructor argument prefix, +field inference origin, completed application, selected variant, and exact source provenance. A +failed field, qualifier, visibility check, or inference step SHALL retain every independent fact and +make only the dependent construction and parent executability outcomes unavailable. + +#### Scenario: Inspect an inferred variant constructor + +- **WHEN** `Option.Some { value: 42 }` completes its omitted parent argument from the payload field +- **THEN** facts expose the canonical Option declaration, Some variant, value field, `T = i32` inference origin, complete `Option` application, and precise result + +#### Scenario: Preserve a damaged union declaration + +- **WHEN** one variant field is unresolved beside valid siblings +- **THEN** facts retain the canonical parent and every independent sibling while marking construction, coverage, layout, and execution of the complete parent unavailable + +### Requirement: Parent-union projections remain explicit failures + +A projection fact whose subject is a nominal union SHALL retain the subject type, requested spelling, +candidate variant fields, exact provenance, and unavailable outcome. It MUST NOT synthesize a common +field identity from same-spelled fields in multiple variants or expose an inactive payload place. + +#### Scenario: Inspect a rejected parent projection + +- **WHEN** `result.value` is analyzed for `Result` +- **THEN** facts retain the Result subject and requested field while the projection remains unavailable until a variant pattern binds the payload + +## MODIFIED Requirements + +### Requirement: Match facts retain source arms and canonical coverage + +Semantic analysis SHALL publish the scrutinee type and access mode, source-ordered arms, resolved +structural roots, applied nominal parents, canonical variants, complete selection paths, source and +canonical field mappings, pattern bindings, guard outcomes, remaining path set before and after each +arm, reachability, result type, and complete-or-unavailable match outcome. Whole-member selection +SHALL retain the covered descendant paths, while direct variant selection SHALL retain its exact +root-parent-variant leaf without representing that leaf as a structural member. Failed lookups, +damaged patterns, incompatible guards, and unavailable results SHALL retain all independent facts +with exact provenance and causal diagnostics. + +#### Scenario: Inspect coverage arm by arm + +- **WHEN** `Token` and `End` unguarded arms cover `Token | End` +- **THEN** facts show the canonical set before each arm and the empty remaining set after the second + +#### Scenario: Retain an unknown member pattern + +- **WHEN** one arm names an unresolved nominal type beside an independently valid arm +- **THEN** both arm facts remain queryable and only the dependent match outcome is unavailable + +#### Scenario: Inspect hierarchical coverage arm by arm + +- **WHEN** direct variant arms cover every leaf of `HttpError` inside `HttpError | OutOfMemoryError` +- **THEN** facts retain each complete selection path, each subtraction step, and the unchanged normalized structural root identities diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md new file mode 100644 index 000000000..8a09be0d1 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md @@ -0,0 +1,55 @@ +## MODIFIED Requirements + +### Requirement: Option is ordinary canonical Silk source + +The standard library SHALL define `Option` as an ordinary shipped nominal union with unit +variant `None` and named-field variant `Some { pub value: T }`. The parent union SHALL be public, so +its variants are externally selectable, and the payload field SHALL be public for direct construction +and matching. Recoverable integer operations and every other optional-value consumer SHALL use this +declaration without an Option-shaped compiler collection primitive. The ordinary `some` and `none` +helper functions MAY remain as ergonomic constructors only when they construct the direct variants. +The former transparent wrapper struct, detached `Some` and `None` structs, compatibility aliases, +and dual representations MUST NOT remain. + +#### Scenario: Return checked success + +- **WHEN** checked integer arithmetic succeeds +- **THEN** it returns the canonical `Option.Some` variant containing the exact value + +#### Scenario: Return checked failure + +- **WHEN** checked integer arithmetic cannot represent a result +- **THEN** it returns canonical `Option.None` + +#### Scenario: Remove the wrapper representation + +- **WHEN** standard-library source, manifests, documentation, and tests are inspected after migration +- **THEN** `Option` is the direct nominal union and no detached `Some`, detached `None`, wrapper `value` field, alias, or compatibility path remains + +## ADDED Requirements + +### Requirement: Result is one ordinary nominal union + +The standard library SHALL define `Result` as an ordinary shipped nominal union with +`Success { pub value: A }` and `Failure { pub error: E }`. The parent union SHALL be public, so its +variants are externally selectable, and both payload fields SHALL be public for direct construction +and matching. Its error argument MAY itself be an ordinary structural union and SHALL normalize +independently without changing the two Result variants. The ordinary `succeed` and `failResult` +helper functions MAY remain as ergonomic constructors only when they construct the direct variants. +The former wrapper, detached `Success` and `Failure` declarations, compatibility aliases, and +dual representations MUST NOT remain. + +#### Scenario: Carry a structural failure set + +- **WHEN** a function returns `Result` +- **THEN** the result retains exactly `Success` and `Failure`, and the `Failure.error` payload retains the independently normalized structural union + +#### Scenario: Migrate standard-library operations + +- **WHEN** `map`, `mapError`, `flatMap`, predicates, `Effect.result`, and other Result producers or consumers are compiled +- **THEN** they construct and match direct Result variants without a wrapper field or detached member types + +#### Scenario: Remove the Result wrapper representation + +- **WHEN** standard-library source, manifests, callers, fixtures, documentation, and tests are inspected after migration +- **THEN** `Result` is the direct nominal union and no detached member, wrapper `value` field, alias, compatibility path, or dual representation remains diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-standard-input/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-standard-input/spec.md new file mode 100644 index 000000000..26bbceac5 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-standard-input/spec.md @@ -0,0 +1,26 @@ +## MODIFIED Requirements + +### Requirement: The native provider reads through one unsafe OS primitive + +Canonical standard-library source SHALL define `OsStandardInput` as an ordinary provider that reads +the process standard-input descriptor through one unsafe `Intrinsic` operation returning `bool` and +writing transferred count, low-level reason, and native code to explicit initialized scalar outputs. +A successful zero-length transfer SHALL become `EndOfInput`; a successful positive count SHALL become +`Filled`; and `false` SHALL become `StreamReadError`. No compiler phase MAY construct Option or +recognize the `StandardInput`, `OsStandardInput`, `ReadOutcome`, or `read` spellings to select special +behavior. + +#### Scenario: Read through the native implementation + +- **WHEN** a provided native implementation receives one exclusive buffer +- **THEN** its source operation invokes one primitive read boundary and preserves the service's outcome and typed failure + +#### Scenario: Reject the native read on direct WebAssembly + +- **WHEN** a reachable native read is compiled for a direct WebAssembly target +- **THEN** target availability rejects it rather than inventing an input host import + +#### Scenario: Link only the reachable runtime symbol + +- **WHEN** a native program reads standard input and touches no filesystem +- **THEN** the artifact links the byte-input runtime symbol and no filesystem runtime symbol diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-structural-unions/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-structural-unions/spec.md new file mode 100644 index 000000000..4398555db --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-structural-unions/spec.md @@ -0,0 +1,46 @@ +## ADDED Requirements + +### Requirement: A nominal union is one atomic structural-union member + +A complete represented nominal union MAY be one ordinary member of `A | B`. Structural +normalization SHALL use the applied parent union type as that member's identity and SHALL NOT flatten +its variants, merge a variant with another structural member, or expose its private tag. Ordinary +contextual injection and widening SHALL first construct or preserve the nominal union value and then +map that whole value as one structural member. + +#### Scenario: Inject a nominal variant into a structural union + +- **WHEN** `HttpError.Dns { ... }` enters an expected `HttpError | OutOfMemoryError` +- **THEN** construction first produces `HttpError` and structural conversion injects that complete nominal value as one member + +#### Scenario: Preserve the nominal boundary during normalization + +- **WHEN** one `HttpError` variant has the same payload shape as another structural member +- **THEN** normalization retains `HttpError` and the other member as distinct types and does not flatten the matching variant + +## MODIFIED Requirements + +### Requirement: Pattern selection uses exact normalized union members + +Pattern analysis SHALL select ordinary structural-union roots by canonical normalized type identity. +Nominal values, scalars, arrays, strings, and finite represented executable members MAY be exact +whole-member selectors when they are valid members of the scrutinee. When an exact root is a nominal +union, a qualified variant pattern MAY additionally select one subordinate canonical variant leaf. +That selection SHALL retain a canonical path containing the structural root, applied nominal parent, +and variant; it SHALL NOT turn the variant into a structural-union member, invent a second membership +relation, or expose either numeric runtime tag. + +#### Scenario: Select a scalar member + +- **WHEN** an `i32 | string` value is matched by an `i32 number` pattern +- **THEN** the selected binding has exact type `i32` and coverage removes that canonical member + +#### Scenario: Reject a foreign exact member + +- **WHEN** a pattern selects `bool` from `i32 | string` +- **THEN** analysis identifies `bool` as absent from the normalized scrutinee members + +#### Scenario: Select a subordinate nominal variant + +- **WHEN** `HttpError.Dns { .. }` matches through structural root `HttpError` in `HttpError | OutOfMemoryError` +- **THEN** selection retains the complete root-parent-variant path while the normalized structural member set remains exactly `HttpError | OutOfMemoryError` diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-syntax/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-syntax/spec.md new file mode 100644 index 000000000..a68d23180 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-syntax/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Nominal union syntax is lossless and recoverable + +The parser SHALL retain optional declaration visibility, the `union` keyword, name, optional type +parameters, ordered unit and named-field variants, field visibility and types, separators, braces, +comments, and unavailable recovery elements in one lossless union CST. Expression and pattern +syntax SHALL parse a nominal union path with an optional contiguous explicit generic prefix followed +by a dot and variant name. A constructor MAY then have a named-field body; a pattern SHALL use a +complete applied parent and MAY have the selected variant's named-field pattern body. + +#### Scenario: Parse a generic mixed union + +- **WHEN** source declares `union Option { None, Some { pub value: T } }` +- **THEN** the CST retains the parent type parameter and distinct unit variant, field variant, and field nodes with exact spans + +#### Scenario: Parse an applied variant constructor + +- **WHEN** an expression spells `Result.Success { value: 42 }` +- **THEN** the CST treats `Result` as the applied parent qualifier and `Success` as its variant rather than attaching the arguments to a detached member + +#### Scenario: Parse a constructor with an omitted parent suffix + +- **WHEN** an expression spells `Option.Some { value: 42 }` +- **THEN** the CST retains `Option` as the unapplied parent declaration path and leaves generic completion to semantic field inference + +#### Scenario: Recover within one damaged variant + +- **WHEN** a named-field variant has a missing field type or closing brace beside valid sibling variants +- **THEN** recovery remains within that declaration and preserves the valid siblings as available syntax diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md new file mode 100644 index 000000000..0a8149d74 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md @@ -0,0 +1,68 @@ +## ADDED Requirements + +### Requirement: Nominal union layout is a compiler-owned tagged payload plan + +Every complete non-generic nominal union SHALL receive a target-aware catalog entry before runtime +reachability, including unused private declarations. Every reachable concrete generic application +SHALL receive one specialized entry, while an open generic declaration SHALL receive no speculative +physical layout. Each available entry SHALL contain an inaccessible variant tag, one payload offset, +and storage aligned and sized for its largest concrete variant payload. Unit variants SHALL require +no payload bytes. The plan SHALL retain canonical parent, variant, field, ordinal, availability, +size, alignment, and padding metadata; source semantics SHALL expose no numeric tag, stable external +ABI, or serialization representation. + +#### Scenario: Plan mixed unit and payload variants + +- **WHEN** a concrete union contains one unit variant and payload variants with distinct sizes and alignments +- **THEN** the layout contains one tag and one correctly aligned payload region sufficient for every variant with deterministic padding + +#### Scenario: Specialize a generic union layout + +- **WHEN** `Option` is reachable as `Option` and `Option` +- **THEN** layout planning produces separate finite concrete entries from the same canonical variant set and each calling shape consumes its selected entry + +#### Scenario: Catalog an unused non-generic union + +- **WHEN** a module declares a valid private non-generic union that no runtime instance reaches +- **THEN** the nominal catalog exposes its complete target-aware layout while the runtime plan omits it + +#### Scenario: Preserve an unavailable union catalog entry + +- **WHEN** one variant field has an unresolved type +- **THEN** the catalog retains the parent entry and originating unavailable cause without publishing a partial tag or payload plan + +### Requirement: Each variant payload reuses nominal field layout + +Each named-field variant SHALL lay out its specialized fields in declaration order under the same +target-aware offset, alignment, padding, represented-callable, represented-Effect, and unavailable- +dependency rules as a nominal struct. The enclosing union payload region SHALL satisfy the maximum +size and alignment of those complete variant payload layouts. Unit variants SHALL contribute an +empty payload layout and SHALL NOT create source-visible fields. + +#### Scenario: Lay out a padded multi-field variant + +- **WHEN** one variant contains multiple fields whose target alignments require internal and tail padding +- **THEN** its variant plan records the ordinary declaration-ordered field offsets and the union payload region preserves that complete aligned layout + +### Requirement: Nominal union calling shape is compiler-owned target data + +For every reachable nominal-union parameter or result, target planning SHALL publish one +backend-neutral tag-plus-payload calling shape and a complete canonical mapping from every variant's +logical field calling shape into fixed payload slots. Construction, calls, returns, matching, and +cleanup SHALL consume that same mapping. An unavailable variant layout or impossible mapping SHALL +make the calling shape unavailable before MIR or backend emission. + +#### Scenario: Plan a nominal union call boundary + +- **WHEN** a function accepts and returns a union whose variants have different aggregate field shapes +- **THEN** the plan fixes one tag-plus-payload shape and complete per-variant field mappings for both the parameter and result + +### Requirement: Union layout recursion follows nominal aggregate rules + +Layout dependency analysis SHALL reject every inline recursive cycle through union and struct fields +and SHALL accept a cycle only when an existing explicit finite indirection breaks storage recursion. + +#### Scenario: Reject a mixed struct-union cycle + +- **WHEN** a struct stores a union inline and one variant stores the struct inline +- **THEN** layout analysis reports the complete canonical cycle and publishes no partial layout for either declaration diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-type-generics/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-type-generics/spec.md new file mode 100644 index 000000000..07e33e665 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-type-generics/spec.md @@ -0,0 +1,40 @@ +## ADDED Requirements + +### Requirement: Union variants specialize through parent-owned arguments + +A generic union SHALL bind parameters once on its parent declaration. Variant field types, patterns, +ownership evidence, layouts, and constructors SHALL substitute the parent's canonical arguments; +variants SHALL NOT declare or infer an independent generic identity. Named-field construction MAY +supply a contiguous explicit prefix and infer the omitted suffix only from supplied fields under the +existing struct-construction rules. Unit construction and every variant pattern SHALL name a complete +parent application. Expected result types, scrutinee types, and later uses MUST NOT bind missing +arguments. + +#### Scenario: Specialize two payload variants through one parent + +- **WHEN** `Result` is applied as `Result` +- **THEN** `Success.value` specializes to `i32`, `Failure.error` specializes to `Problem`, and both remain variants of the same applied parent + +#### Scenario: Refuse expected-type inference for a unit variant + +- **WHEN** `Option.None` omits `T` and is placed in a declaration expecting `Option` +- **THEN** construction reports `T` as uninferred and requires `Option.None` + +### Requirement: Nominal variants never collapse during specialization + +Every complete union application SHALL preserve the declaration's canonical ordered variant set +even when substitution makes two payload shapes equal, makes a payload uninhabited, or makes the +union representation coincide with another type. Structural unions nested in fields SHALL continue +to renormalize independently. An uninhabited payload SHALL NOT remove its variant from canonical +coverage, tag metadata, or layout; it remains unconstructible unless a valid value of that field type +is supplied. + +#### Scenario: Preserve equal specialized payloads + +- **WHEN** two variants carry `A` and `B` and the parent specializes both as `i32` +- **THEN** both variant identities and runtime alternatives remain distinct while any structural union inside a field follows ordinary normalization + +#### Scenario: Preserve an uninhabited specialized variant + +- **WHEN** `Result.Failure` specializes its payload as `never` +- **THEN** `Failure` retains its canonical coverage leaf and private tag metadata, requires an arm in exhaustive matching, and cannot be constructed without a valid `never` expression diff --git a/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md b/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md new file mode 100644 index 000000000..f4b1da107 --- /dev/null +++ b/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Nominal union declarations have one canonical layout + +The formatter SHALL preserve comments and source meaning while rendering optional visibility, +`union`, the union name and type parameters, braces, source-ordered unit and named-field variants, +field visibility and types, separators, constructors, and patterns canonically. Multiline variants +and fields SHALL use deterministic indentation and trailing separators, and formatting SHALL remain +idempotent without changing variant or field identity. + +#### Scenario: Format a generic mixed union + +- **WHEN** a complete union contains unit and named-field variants with irregular whitespace +- **THEN** formatting emits one canonical generic declaration with stable variant and field indentation and preserves all comments + +#### Scenario: Format an applied variant path + +- **WHEN** construction or a pattern spells `Result.Success { value }` +- **THEN** formatting preserves the applied parent before the dot and formats the field body under the ordinary struct-like policy + diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md new file mode 100644 index 000000000..ec1eb37d6 --- /dev/null +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -0,0 +1,101 @@ +## 1. Syntax, Recovery, and Formatting + +- [ ] 1.1 Add the complete-identifier `union` token to lexical, token-presentation, and generated token consumers, and verify lexer tests distinguish `union` from identifier prefixes. +- [ ] 1.2 Add lossless CST nodes for union declarations, unit variants, named-field variants, and parent-qualified variant selectors, and verify syntax snapshots retain trivia, separators, fields, and exact spans. +- [ ] 1.3 Implement parser entry points for generic union declarations, constructor qualifiers with explicit argument prefixes, and fully applied variant patterns, and verify focused parser tests cover valid mixed variants and reject empty named-field bodies. +- [ ] 1.4 Implement variant-local parser recovery for missing names, types, separators, and braces, and verify damaged-variant tests preserve valid siblings and following declarations. +- [ ] 1.5 Extend the formatter, syntax correspondence, and source presentation for union declarations, constructors, and patterns, and verify formatting is idempotent and preserves comments. +- [ ] 1.6 Add stable structured diagnostic catalog entries for union-specific syntax and semantic failures, regenerate catalog artifacts, and verify diagnostic tests assert codes, spans, related spans, and details rather than message text. + +## 2. Canonical Declarations and Module Surfaces + +- [ ] 2.1 Add canonical `UnionFact`, subordinate variant identities, and variant-scoped field ownership while generalizing shared field facts away from struct-only owners, and verify identity tests distinguish same-spelled variants and fields under different parents. +- [ ] 2.2 Collect unions in the ordinary cross-kind module namespace with parent parameters and source-ordered variants before bodies, and verify forward declarations, duplicates, empty unions, and cross-kind collisions in declaration-index tests. +- [ ] 2.3 Resolve every variant field type, visibility exposure, generic reference, and inline aggregate dependency before body analysis, and verify invalid fields preserve sibling facts while making the complete parent non-executable. +- [ ] 2.4 Encode union declarations in deterministic module semantic surfaces, and verify encode/decode, equality, and dependency invalidation respond to variant order, kind, field, type, visibility, bound, and availability changes but ignore body-only edits. +- [ ] 2.5 Extend semantic occurrence, navigation, completion, documentation, and Analysis facade queries for parent, variant, and field identities, and verify go-to-definition/reference tests use canonical facts rather than syntax reconstruction. + +## 3. Type Application and Variant Construction + +- [ ] 3.1 Teach nominal-type lookup and substitution to distinguish union declarations while keeping the complete parent application as the only value type, and verify no variant type or structural member is created. +- [ ] 3.2 Refactor struct-literal field checking into a shared aggregate-field elaborator without changing existing struct facts or diagnostics, and verify the existing struct construction and generic-inference suites remain byte-for-byte stable where golden data exists. +- [ ] 3.3 Implement two-stage variant constructor resolution—parent declaration and explicit prefix first, field-only suffix inference second—and verify zero-prefix, partial-prefix, conflicting, and parent-only uninferred argument cases. +- [ ] 3.4 Implement unit and named-field construction with complete field initialization, construction authority, visibility fences, type compatibility, represented fields, and precise parent result types, and verify cross-module private fields block raw construction. +- [ ] 3.5 Preserve every variant through generic specialization, including equal and `never` payloads while independently renormalizing structural-union fields, and verify specialization facts never collapse or flatten variants. +- [ ] 3.6 Reject direct parent-union field projection and common-field synthesis while retaining diagnostic facts, and verify `result.value` is unavailable until a variant pattern binds its payload. +- [ ] 3.7 Admit interface, operator, Copy, and Drop declarations against nominal union parents through the ordinary conformance/coherence path, and verify variant names do not become lookup or implementation targets. + +## 4. Variant Patterns and Hierarchical Coverage + +- [ ] 4.1 Extend the shared pattern representation with fully applied variant selectors and struct-like named-field bindings, omissions, nesting, borrows, moves, and writes, and verify unit and payload pattern diagnostics match struct field policy. +- [ ] 4.2 Replace flat match coverage keys with canonical selection paths that retain structural roots, applied nominal parents, and variants, and verify ordinary structural-union and scalar-enum coverage behavior remains unchanged. +- [ ] 4.3 Implement direct variant subtraction through structural-union roots plus whole-parent subtree subtraction, and verify exhaustive, duplicate, unreachable, wildcard, and fully qualified missing-path diagnostics. +- [ ] 4.4 Preserve nominal union roots as atomic `A | B` members during injection, widening, normalization, pattern selection, and specialization, and verify matching a leaf never changes the structural member set. +- [ ] 4.5 Keep guarded affine variant selections provisional until guard success, and verify a false guard leaves both tag levels, complete payload ownership, and cleanup available to a later arm. +- [ ] 4.6 Cover generic and uninhabited cases, and verify `Option | Option` retains distinct fully applied paths and `Result.Failure` remains a required coverage leaf without becoming constructible. + +## 5. Ownership, Represented Fields, and Cleanup + +- [ ] 5.1 Apply affine-by-default ownership and explicit Copy validation across every specialized variant field, and verify all-Copy payloads remain affine without `impl Copy` while one affine field rejects the implementation. +- [ ] 5.2 Build active-variant cleanup plans that reuse nominal Drop ordering and clean only initialized fields of the selected variant, and verify success, typed-failure, and ordinary scope exits release each owned payload exactly once. +- [ ] 5.3 Implement moved and borrowed variant-pattern ownership, including branch-local cleanup of omitted fields and rejection of invalid partial moves, and verify extracted and omitted fields have one final owner. +- [ ] 5.4 Realize callable-bounded fields only inside the active variant using exact static callable storage and access rules, and verify unsupported representations retain the pre-MIR storage fence. +- [ ] 5.5 Realize Effect-bounded fields only inside the active variant with lazy runner, environment, suspension, access, and cleanup facts, and verify unsupported shapes retain the pre-MIR storage fence. + +## 6. Target Layout and Calling Shapes + +- [ ] 6.1 Extend the inline dependency graph and nominal layout catalog to include complete non-generic unions and mixed struct/union cycles, and verify unused private and unavailable union entries appear before runtime reachability. +- [ ] 6.2 Add a distinct nominal-union representation plan with deterministic private tags, source-order ordinals, payload offset, maximum size/alignment, total padding, and per-variant aggregate layouts, and verify unit, padded multi-field, and `never` payload cases. +- [ ] 6.3 Specialize reachable generic union layouts without speculative open-generic entries, and verify equivalent concrete applications reuse one catalog identity while distinct applications receive distinct physical plans. +- [ ] 6.4 Publish a backend-neutral tag-plus-payload calling shape with complete per-variant logical-field mappings, and verify call/return plans for heterogeneous variants are deterministic and unavailable dependencies stop before MIR. +- [ ] 6.5 Extend layout encoding, verification, and Analysis projections with nominal-union facts under unambiguous internal names, and verify no nominal tag, padding, or ABI detail becomes source-observable. + +## 7. HIR, MIR, and Verification + +- [ ] 7.1 Add explicit HIR construction and variant-selection nodes carrying applied parent, canonical variant, specialized fields, source mapping, access, selection path, and cleanup identity, and verify HIR snapshots retain both outer and inner selections. +- [ ] 7.2 Lower union construction and hierarchical patterns through the verified layout and ownership plans, and verify a direct nested arm produces an outer structural decision followed by the nominal variant decision. +- [ ] 7.3 Add monomorphic MIR operations for nominal construction, tag selection, dominated payload projection, and active copy/drop dispatch, and verify MIR rejects foreign parents, fields, layouts, tags, and inactive cleanup. +- [ ] 7.4 Extend MIR verification with selection-dominance and hierarchical-coverage checks, and verify an incomplete path or backend-default fallback is rejected before execution. +- [ ] 7.5 Add deterministic nominal-union MIR encoding and committed in-process goldens, and verify equivalent discovery traversals produce identical instance, variant, field, path, layout, and cleanup ordering. + +## 8. Evaluation and Backends + +- [ ] 8.1 Represent evaluator values by semantic parent, active variant, and complete payload, and verify construction, movement, matching, storage, calls, returns, and active cleanup without evaluating inactive storage. +- [ ] 8.2 Implement direct WebAssembly nominal-union construction, transport, nested tag dispatch, payload mapping, and active cleanup from verified MIR/layout plans, and verify focused Wasm tests cover codegen-specific representation claims. +- [ ] 8.3 Implement native LLVM nominal-union construction, transport, nested tag dispatch, payload mapping, and active cleanup from the same plans, and verify target-specific lowering tests contain no backend-owned tag or offset decisions. +- [ ] 8.4 Add representative unit, payload, generic, represented-field, structural-root, and cleanup programs to the shared evaluator/Wasm assertions and native differential corpus, and verify all engines agree without adding per-feature native-agreement tests. + +## 9. Carrier-Neutral Intrinsic Migration + +- [ ] 9.1 Replace checked scalar intrinsic result contracts with generic present/absent exact `once fn` carriers while keeping the intrinsic operation inventory count unchanged, and verify catalog audit tests contain no Option identity or spelling. +- [ ] 9.2 Lower and execute checked carrier selection with exactly one callback invocation and cleanup of the unused callable environment, and verify evaluator, Wasm, and native tests cover success, absence, affine captures, and traps. +- [ ] 9.3 Replace abstraction-shaped completed-Effect reification with a carrier-neutral success/failure fold preserving requirement rows, access, cleanup, laziness, and suspension, and verify an equivalent user wrapper can select another nominal carrier without compiler registration. +- [ ] 9.4 Replace handle-producing file and directory open results with affine-safe success/failure `once fn` carriers, and verify success transfers one initialized `OsHandle` plus close obligation while failure creates no handle or optionally initialized place. +- [ ] 9.5 Replace optional count-producing OS filesystem, standard-input, child-process, and process-input results with primitive `bool` plus initialized count/reason/code outputs, and verify host-boundary tests distinguish zero-length success, absence, and refusal without constructing Option in compiler code. +- [ ] 9.6 Remove `Type.option`, old Result/member helpers, detached outcome construction, and Option/Result-specific branches from analysis, HIR, MIR, evaluation, and backends, and verify repository searches plus intrinsic audits find no compiler recognition by standard-library module or declaration spelling. + +## 10. Atomic Standard-Library Migration + +- [ ] 10.1 Replace `option.silk` with the public nominal union and direct `some`/`none` helpers, and verify its combinators construct and match direct variants with public payload access and no wrapper field. +- [ ] 10.2 Replace `result.silk` with the public nominal union and direct `succeed`/`failResult` helpers, and verify its combinators accept structural error unions without flattening Success or Failure. +- [ ] 10.3 Update integer, character, string, allocation, and other checked wrappers to supply carrier-neutral intrinsic adapters and return direct Option variants, and verify checked success/absence tests use the canonical nominal representation. +- [ ] 10.4 Update `Effect.result`, Effect combinators, and every direct intrinsic outcome consumer to construct and match direct Result variants, and verify success/failure reification has exactly one nominal layer. +- [ ] 10.5 Migrate filesystem, process, formatting, random, collection, and remaining canonical Silk modules from detached member imports and wrapper-field matches to qualified parent variants, and verify the complete stdlib source closure compiles. +- [ ] 10.6 Delete detached `Some`, `None`, `Success`, and `Failure` declarations, wrapper structs, aliases, dual paths, stale imports, and old generated embeddings, then regenerate the deterministic stdlib manifest and verify a repository-wide removal test finds no superseded representation. + +## 11. Tooling, Documentation, and Acceptance + +- [ ] 11.1 Extend syntax highlighting/token consumers, hover, completion, signature help, rename, references, and inspector/labs projections for union declarations and qualified variants, and verify LSP and tooling snapshots navigate through canonical parent/variant/field facts. +- [ ] 11.2 Add the prescriptive nominal-union reference documentation covering declaration syntax, generic qualification/inference, visibility, ownership, layout abstraction, matching, and distinction from `enum` and `A | B`, and verify documentation examples compile as doctests where supported. +- [ ] 11.3 Rewrite Option, Result, Effect, integer, and error-model documentation/examples for qualified direct variants and structural error composition, and verify no documentation search finds detached member types or wrapper `.value` matches. +- [ ] 11.4 Add or update acceptance corpus cases for `Result`, direct hierarchical matching, generic variants, Copy/Drop, recursion rejection, represented fields, and diagnostics, and verify each claim is tested at the cheapest policy-approved tier. + +## 12. Final Verification + +- [ ] 12.1 Run the focused lexer, parser, formatter, declaration, semantic, matching, ownership, layout, HIR, MIR, evaluator, Wasm, native-corpus, intrinsic, stdlib, LSP, and doctest suites and verify every delta-spec scenario has direct evidence. +- [ ] 12.2 Run `pnpm typecheck` and fix every introduced type error, recording any unrelated pre-existing failure exactly. +- [ ] 12.3 Run `pnpm exec biome check .` and fix every introduced formatting or lint failure, recording any unrelated pre-existing failure exactly. +- [ ] 12.4 Run `pnpm test` and fix every introduced test failure, recording any unrelated pre-existing failure exactly. +- [ ] 12.5 Run `pnpm check` and verify the repository-wide required gate completes, or report the exact pre-existing blocker without describing the change as complete. +- [ ] 12.6 Run `pnpm release:candidate` because compiler package contents change, and verify package contents, exports, stdlib embeddings, and release artifacts are internally consistent. +- [ ] 12.7 Run `openspec validate add-nominal-unions --strict` and verify proposal, all delta specs, design, and tasks remain coherent after implementation discoveries. From a88e149c8545421daa2c62d915e305eeb521e052 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 12:54:35 -0300 Subject: [PATCH 02/42] feat(compiler): parse nominal union syntax --- openspec/changes/add-nominal-unions/tasks.md | 10 +- packages/compiler/src/ImportPath.ts | 1 + packages/compiler/src/Lexer.ts | 1 + packages/compiler/src/Parser/Declaration.ts | 119 ++++++++++ packages/compiler/src/Parser/Expression.ts | 224 +++++++++++++++++- packages/compiler/src/Parser/Grammar.ts | 2 + packages/compiler/src/Parser/Statement.ts | 2 + packages/compiler/src/Parser/Type.ts | 4 + packages/compiler/src/SyntaxFormatter.ts | 141 ++++++++++- packages/compiler/src/SyntaxTree.ts | 6 + packages/compiler/src/Token.ts | 2 + .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/test/Lexer.test.ts | 11 + packages/compiler/test/LexerPressure.test.ts | 2 + packages/compiler/test/Parser.test.ts | 56 +++++ .../compiler/test/SyntaxFormatter.test.ts | 68 ++++++ packages/compiler/test/goldens/canonical.silk | 13 + packages/editor-support/src/CodeMirror.ts | 1 + packages/editor-support/src/TextMate.ts | 1 + 19 files changed, 658 insertions(+), 8 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index ec1eb37d6..c9a3db812 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -1,10 +1,10 @@ ## 1. Syntax, Recovery, and Formatting -- [ ] 1.1 Add the complete-identifier `union` token to lexical, token-presentation, and generated token consumers, and verify lexer tests distinguish `union` from identifier prefixes. -- [ ] 1.2 Add lossless CST nodes for union declarations, unit variants, named-field variants, and parent-qualified variant selectors, and verify syntax snapshots retain trivia, separators, fields, and exact spans. -- [ ] 1.3 Implement parser entry points for generic union declarations, constructor qualifiers with explicit argument prefixes, and fully applied variant patterns, and verify focused parser tests cover valid mixed variants and reject empty named-field bodies. -- [ ] 1.4 Implement variant-local parser recovery for missing names, types, separators, and braces, and verify damaged-variant tests preserve valid siblings and following declarations. -- [ ] 1.5 Extend the formatter, syntax correspondence, and source presentation for union declarations, constructors, and patterns, and verify formatting is idempotent and preserves comments. +- [x] 1.1 Add the complete-identifier `union` token to lexical, token-presentation, and generated token consumers, and verify lexer tests distinguish `union` from identifier prefixes. +- [x] 1.2 Add lossless CST nodes for union declarations, unit variants, named-field variants, and parent-qualified variant selectors, and verify syntax snapshots retain trivia, separators, fields, and exact spans. +- [x] 1.3 Implement parser entry points for generic union declarations, constructor qualifiers with explicit argument prefixes, and fully applied variant patterns, and verify focused parser tests cover valid mixed variants and reject empty named-field bodies. +- [x] 1.4 Implement variant-local parser recovery for missing names, types, separators, and braces, and verify damaged-variant tests preserve valid siblings and following declarations. +- [x] 1.5 Extend the formatter, syntax correspondence, and source presentation for union declarations, constructors, and patterns, and verify formatting is idempotent and preserves comments. - [ ] 1.6 Add stable structured diagnostic catalog entries for union-specific syntax and semantic failures, regenerate catalog artifacts, and verify diagnostic tests assert codes, spans, related spans, and details rather than message text. ## 2. Canonical Declarations and Module Surfaces diff --git a/packages/compiler/src/ImportPath.ts b/packages/compiler/src/ImportPath.ts index 444f7850a..867caf419 100644 --- a/packages/compiler/src/ImportPath.ts +++ b/packages/compiler/src/ImportPath.ts @@ -6,6 +6,7 @@ const reservedSegmentKinds: ReadonlySet = new Set([ 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'ServiceKeyword', 'InterfaceKeyword', 'RoleKeyword', diff --git a/packages/compiler/src/Lexer.ts b/packages/compiler/src/Lexer.ts index 252ce2866..475ddd32b 100644 --- a/packages/compiler/src/Lexer.ts +++ b/packages/compiler/src/Lexer.ts @@ -109,6 +109,7 @@ const keywordSpellings: ReadonlyArray = Obje ['const', 'ConstKeyword'], ['struct', 'StructKeyword'], ['enum', 'EnumKeyword'], + ['union', 'UnionKeyword'], ['service', 'ServiceKeyword'], ['interface', 'InterfaceKeyword'], ['role', 'RoleKeyword'], diff --git a/packages/compiler/src/Parser/Declaration.ts b/packages/compiler/src/Parser/Declaration.ts index cc8932134..2327bc4bf 100644 --- a/packages/compiler/src/Parser/Declaration.ts +++ b/packages/compiler/src/Parser/Declaration.ts @@ -43,6 +43,7 @@ export const beginsTopLevelDeclaration = (state: State): boolean => { kind === 'UnsafeKeyword' || kind === 'StructKeyword' || kind === 'EnumKeyword' || + kind === 'UnionKeyword' || kind === 'ServiceKeyword' || kind === 'InterfaceKeyword' || kind === 'RoleKeyword' || @@ -58,6 +59,7 @@ export const beginsTopLevelDeclaration = (state: State): boolean => { (peek(state, 2) === 'FnKeyword' || peek(state, 2) === 'EffectKeyword')) || following === 'StructKeyword' || following === 'EnumKeyword' || + following === 'UnionKeyword' || following === 'ServiceKeyword' || following === 'InterfaceKeyword' || following === 'RoleKeyword' || @@ -263,6 +265,120 @@ export const parseStructDeclaration = (initial: State): NodeResult => { }) } +export const parseUnionVariantField = (initial: State): NodeResult => { + const field = parseStructField(initial) + return Object.freeze({ + state: field.state, + node: syntaxNode(field.state, 'UnionVariantField', field.node.children), + }) +} + +export const parseUnionVariant = (initial: State): NodeResult => { + const name = expect(initial, 'Identifier', [ + 'LeftBrace', + 'Comma', + 'RightBrace', + ...topLevelFollowing, + ]) + if (nextSignificantKind(name.state) !== 'LeftBrace') { + return Object.freeze({ + state: name.state, + node: syntaxNode(name.state, 'UnionVariant', name.elements), + }) + } + + const left = expect(name.state, 'LeftBrace', [ + 'PubKeyword', + 'Identifier', + 'RightBrace', + ...topLevelFollowing, + ]) + let state = left.state + let children: ReadonlyArray = Object.freeze([ + ...name.elements, + ...left.elements, + ]) + + if (nextSignificantKind(state) === 'RightBrace') { + const field = parseUnionVariantField(state) + children = Object.freeze([...children, field.node]) + state = field.state + } else { + while ( + !beginsTopLevelDeclaration(state) && + nextSignificantKind(state) !== 'RightBrace' && + nextSignificantKind(state) !== 'EndOfFile' + ) { + const field = parseUnionVariantField(state) + children = Object.freeze([...children, field.node]) + state = field.state + if (nextSignificantKind(state) === 'RightBrace') break + const comma = expect(state, 'Comma', [ + 'PubKeyword', + 'Identifier', + 'RightBrace', + ...topLevelFollowing, + ]) + children = Object.freeze([...children, ...comma.elements]) + state = comma.state + if (nextSignificantKind(state) === 'RightBrace') break + } + } + + const right = expect(state, 'RightBrace', ['Comma', 'Identifier', ...topLevelFollowing]) + return Object.freeze({ + state: right.state, + node: syntaxNode(right.state, 'UnionVariant', [...children, ...right.elements]), + }) +} + +export const parseUnionDeclaration = (initial: State): NodeResult => { + const hasPublicModifier = nextSignificantKind(initial) === 'PubKeyword' + const pubKeyword = hasPublicModifier + ? expect(initial, 'PubKeyword', ['UnionKeyword', 'Identifier', 'LeftBrace']) + : Object.freeze({ state: initial, elements: Object.freeze([]) }) + const keyword = expect(pubKeyword.state, 'UnionKeyword', ['Identifier', 'LeftBrace']) + const name = expect(keyword.state, 'Identifier', ['Less', 'LeftBrace', ...topLevelFollowing]) + const typeParameters = + nextSignificantKind(name.state) === 'Less' + ? parseTypeParameterList(name.state, ['LeftBrace']) + : undefined + const left = expect(typeParameters?.state ?? name.state, 'LeftBrace', [ + 'Identifier', + 'RightBrace', + ...topLevelFollowing, + ]) + let state = left.state + let children: ReadonlyArray = Object.freeze([ + ...pubKeyword.elements, + ...keyword.elements, + ...name.elements, + ...(typeParameters === undefined ? [] : [typeParameters.node]), + ...left.elements, + ]) + + while ( + !beginsTopLevelDeclaration(state) && + nextSignificantKind(state) !== 'RightBrace' && + nextSignificantKind(state) !== 'EndOfFile' + ) { + const variant = parseUnionVariant(state) + children = Object.freeze([...children, variant.node]) + state = variant.state + if (nextSignificantKind(state) === 'RightBrace') break + const comma = expect(state, 'Comma', ['Identifier', 'RightBrace', ...topLevelFollowing]) + children = Object.freeze([...children, ...comma.elements]) + state = comma.state + if (nextSignificantKind(state) === 'RightBrace') break + } + + const right = expect(state, 'RightBrace', topLevelFollowing) + return Object.freeze({ + state: right.state, + node: syntaxNode(right.state, 'UnionDeclaration', [...children, ...right.elements]), + }) +} + export const serviceOperationFollowing: ReadonlyArray = Object.freeze([ 'FnKeyword', 'EffectKeyword', @@ -478,6 +594,7 @@ const parseServiceLikeDeclaration = (initial: State, kind: 'Service' | 'Interfac nextSignificantKind(state) !== 'ConstKeyword' && nextSignificantKind(state) !== 'StructKeyword' && nextSignificantKind(state) !== 'EnumKeyword' && + nextSignificantKind(state) !== 'UnionKeyword' && nextSignificantKind(state) !== 'ServiceKeyword' && nextSignificantKind(state) !== 'InterfaceKeyword' && nextSignificantKind(state) !== 'ImplKeyword' @@ -590,6 +707,7 @@ export const parseTopLevelDeclaration = (state: State): NodeResult => { if (kind === 'StructKeyword' || following === 'StructKeyword') return parseStructDeclaration(state) if (kind === 'EnumKeyword' || following === 'EnumKeyword') return parseEnumDeclaration(state) + if (kind === 'UnionKeyword' || following === 'UnionKeyword') return parseUnionDeclaration(state) return parseFunctionDeclaration(state) } @@ -604,6 +722,7 @@ export const parseFunctionDeclaration = (initial: State, allowDropName = false): lookaheadToken.kind !== 'UnsafeKeyword' && lookaheadToken.kind !== 'StructKeyword' && lookaheadToken.kind !== 'EnumKeyword' && + lookaheadToken.kind !== 'UnionKeyword' && lookaheadToken.kind !== 'ServiceKeyword' && lookaheadToken.kind !== 'InterfaceKeyword' && lookaheadToken.kind !== 'RoleKeyword' && diff --git a/packages/compiler/src/Parser/Expression.ts b/packages/compiler/src/Parser/Expression.ts index 289c18381..5f1c5affb 100644 --- a/packages/compiler/src/Parser/Expression.ts +++ b/packages/compiler/src/Parser/Expression.ts @@ -94,6 +94,50 @@ export const hasCompleteAppliedPostfix = ( return false } +/** True for a complete applied parent followed by one subordinate variant name. */ +export const hasAppliedUnionVariant = (state: State): boolean => { + let index = state.index + const significant = (): Token.Token | undefined => { + let token = state.lexical.tokens.at(index) + while (token !== undefined && isTrivia(token.kind)) { + index += 1 + token = state.lexical.tokens.at(index) + } + return token + } + if (significant()?.kind !== 'Identifier') return false + index += 1 + if (significant()?.kind === 'Dot') { + index += 1 + if (significant()?.kind !== 'Identifier') return false + index += 1 + } + if (significant()?.kind !== 'Less') return false + let depth = 0 + while (index < state.lexical.tokens.length) { + const token = significant() + if (token === undefined) return false + if (token.kind === 'Less') depth += 1 + else if (token.kind === 'Greater') { + depth -= 1 + if (depth === 0) { + index += 1 + if (significant()?.kind !== 'Dot') return false + index += 1 + return significant()?.kind === 'Identifier' + } + } + index += 1 + } + return false +} + +export const hasBareUnionVariantFields = (state: State): boolean => + nextSignificantKind(state) === 'Identifier' && + peek(state, 1) === 'Dot' && + peek(state, 2) === 'Identifier' && + peek(state, 3) === 'LeftBrace' + export const parseIntegerLiteralExpression = (initial: State): NodeResult => { if (nextSignificantKind(initial) === 'Minus') { const minus = expect(initial, 'Minus', ['DecimalInteger', ...expressionFollowing]) @@ -200,6 +244,7 @@ export const primaryKind = ( | 'Unsafe' | 'Call' | 'StructLiteral' + | 'UnionVariant' | 'ArrayLiteral' | 'Match' | 'Grouped' @@ -234,6 +279,7 @@ export const primaryKind = ( if (token.kind === 'MatchKeyword') return 'Match' if (token.kind === 'LeftBracket') return 'ArrayLiteral' if (token.kind === 'Identifier') { + if (hasAppliedUnionVariant(state)) return 'UnionVariant' const following = peek(state, 1) if (hasCompleteAppliedPostfix(state, 'LeftParenthesis')) return 'Call' if (hasCompleteAppliedPostfix(state, 'LeftBrace')) { @@ -246,7 +292,7 @@ export const primaryKind = ( if (member === 'LeftParenthesis') return 'Call' const afterMember = peek(state, 3) if (afterMember === 'LeftParenthesis') return 'Call' - if (afterMember === 'LeftBrace') return allowStructLiteral ? 'StructLiteral' : 'Identifier' + if (afterMember === 'LeftBrace') return allowStructLiteral ? 'UnionVariant' : 'Identifier' } return 'Identifier' } @@ -265,6 +311,7 @@ export const primaryKind = ( token.kind === 'PubKeyword' || token.kind === 'StructKeyword' || token.kind === 'EnumKeyword' || + token.kind === 'UnionKeyword' || token.kind === 'FnKeyword' || token.kind === 'EffectKeyword' || token.kind === 'ImportKeyword' || @@ -290,6 +337,7 @@ export const remainingRightParentheses = (state: State): number => { token.kind === 'PubKeyword' || token.kind === 'StructKeyword' || token.kind === 'EnumKeyword' || + token.kind === 'UnionKeyword' || token.kind === 'FnKeyword' || token.kind === 'ImportKeyword' || token.kind === 'EndOfFile' @@ -324,6 +372,7 @@ export const expectCallRightParenthesis = ( 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'FnKeyword', 'ImportKeyword', ]) @@ -337,6 +386,7 @@ export function parseArgumentList(initial: State, reservedForEnclosingCalls: num 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'FnKeyword', 'ImportKeyword', ]) @@ -351,6 +401,7 @@ export function parseArgumentList(initial: State, reservedForEnclosingCalls: num kind !== 'PubKeyword' && kind !== 'StructKeyword' && kind !== 'EnumKeyword' && + kind !== 'UnionKeyword' && kind !== 'FnKeyword' && kind !== 'ImportKeyword' && kind !== 'EndOfFile' @@ -366,6 +417,7 @@ export function parseArgumentList(initial: State, reservedForEnclosingCalls: num kind === 'PubKeyword' || kind === 'StructKeyword' || kind === 'EnumKeyword' || + kind === 'UnionKeyword' || kind === 'FnKeyword' || kind === 'ImportKeyword' ) @@ -378,6 +430,7 @@ export function parseArgumentList(initial: State, reservedForEnclosingCalls: num 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'FnKeyword', 'ImportKeyword', ]) @@ -477,6 +530,7 @@ export function parseStructLiteralExpression( kind !== 'PubKeyword' && kind !== 'StructKeyword' && kind !== 'EnumKeyword' && + kind !== 'UnionKeyword' && kind !== 'FnKeyword' && kind !== 'ImportKeyword' && kind !== 'EndOfFile' @@ -510,6 +564,78 @@ export function parseStructLiteralExpression( }) } +export const parseUnionVariantSelector = ( + initial: State, + following: ReadonlyArray, +): NodeResult => { + const parent = hasAppliedUnionVariant(initial) + ? parseTypePrimary(initial, ['Dot', ...following]) + : (() => { + const name = expect(initial, 'Identifier', ['Dot', ...following]) + return Object.freeze({ + state: name.state, + node: syntaxNode(name.state, 'TypePath', name.elements), + }) + })() + const dot = expect(parent.state, 'Dot', ['Identifier', ...following]) + const variant = expect(dot.state, 'Identifier', following) + return Object.freeze({ + state: variant.state, + node: syntaxNode(variant.state, 'UnionVariantSelector', [ + parent.node, + ...dot.elements, + ...variant.elements, + ]), + }) +} + +export function parseUnionVariantExpression( + initial: State, + reservedForEnclosingCalls: number, +): NodeResult { + const selector = parseUnionVariantSelector(initial, ['LeftBrace', ...expressionFollowing]) + if (nextSignificantKind(selector.state) !== 'LeftBrace') { + return Object.freeze({ + state: selector.state, + node: syntaxNode(selector.state, 'UnionVariantExpression', [selector.node]), + }) + } + const left = expect(selector.state, 'LeftBrace', [ + 'Identifier', + 'RightBrace', + ...expressionFollowing, + ]) + let state = left.state + let children: ReadonlyArray = Object.freeze([selector.node, ...left.elements]) + while ( + nextSignificantKind(state) !== 'RightBrace' && + nextSignificantKind(state) !== 'EndOfFile' && + !expressionFollowing.includes(nextSignificantKind(state) ?? 'EndOfFile') + ) { + const field = expect(state, 'Identifier', ['Colon', ...expressionStarts, 'RightBrace']) + const colon = expect(field.state, 'Colon', [...expressionStarts, 'Comma', 'RightBrace']) + const value = parseExpression(colon.state, reservedForEnclosingCalls, 'Identifier') + children = Object.freeze([ + ...children, + syntaxNode(value.state, 'StructFieldInitializer', [ + ...field.elements, + ...colon.elements, + value.node, + ]), + ]) + state = value.state + if (nextSignificantKind(state) === 'RightBrace') break + const comma = expect(state, 'Comma', ['Identifier', 'RightBrace', ...expressionFollowing]) + children = Object.freeze([...children, ...comma.elements]) + state = comma.state + } + const right = expect(state, 'RightBrace', expressionFollowing) + return Object.freeze({ + state: right.state, + node: syntaxNode(right.state, 'UnionVariantExpression', [...children, ...right.elements]), + }) +} + export function parseGroupedExpression( initial: State, reservedForEnclosingCalls: number, @@ -616,6 +742,8 @@ export const isRowWithoutStart = (state: State): boolean => export const isNominalPatternStart = (state: State): boolean => { if (nextSignificantKind(state) !== 'Identifier') return false + if (hasAppliedUnionVariant(state)) return true + if (hasBareUnionVariantFields(state)) return true if (hasCompleteAppliedPostfix(state, 'LeftBrace')) return true const following = peek(state, 1) if (following === 'LeftBrace') return true @@ -715,6 +843,8 @@ export function parsePattern( }) } if (isEnumMemberPatternStart(initial)) return parseEnumMemberPattern(initial) + if (hasAppliedUnionVariant(initial) || hasBareUnionVariantFields(initial)) + return parseUnionVariantPattern(initial) const kind = nextSignificantKind(initial) if (kind === 'DecimalInteger' || (kind === 'Minus' && peek(initial, 1) === 'DecimalInteger')) return parseIntegerPattern(initial) @@ -732,6 +862,95 @@ export function parsePattern( : parseNominalPattern(initial) } +export function parseUnionVariantPattern(initial: State): NodeResult { + const selector = parseUnionVariantSelector(initial, [ + 'LeftBrace', + 'IfKeyword', + 'FatArrow', + 'RightBrace', + ]) + if (nextSignificantKind(selector.state) !== 'LeftBrace') { + return Object.freeze({ + state: selector.state, + node: syntaxNode(selector.state, 'UnionVariantPattern', [selector.node]), + }) + } + const left = expect(selector.state, 'LeftBrace', [ + 'Identifier', + 'DotDot', + 'RightBrace', + 'IfKeyword', + 'FatArrow', + ]) + let state = left.state + let children: ReadonlyArray = Object.freeze([selector.node, ...left.elements]) + while ( + nextSignificantKind(state) !== 'RightBrace' && + nextSignificantKind(state) !== 'IfKeyword' && + nextSignificantKind(state) !== 'FatArrow' && + nextSignificantKind(state) !== 'EndOfFile' + ) { + if (nextSignificantKind(state) === 'DotDot') { + const rest = expect(state, 'DotDot', ['Comma', 'RightBrace', 'IfKeyword', 'FatArrow']) + children = Object.freeze([...children, syntaxNode(rest.state, 'RestPattern', rest.elements)]) + state = rest.state + } else { + const name = expect(state, 'Identifier', [ + 'Colon', + 'Comma', + 'RightBrace', + 'IfKeyword', + 'FatArrow', + ]) + state = name.state + let fieldChildren: ReadonlyArray = name.elements + if (nextSignificantKind(state) === 'Colon' || isNominalPatternStart(state)) { + const colon = expect(state, 'Colon', [ + 'Identifier', + 'Comma', + 'RightBrace', + 'IfKeyword', + 'FatArrow', + ]) + state = colon.state + if (isNominalPatternStart(state)) { + const nested = + hasAppliedUnionVariant(state) || hasBareUnionVariantFields(state) + ? parseUnionVariantPattern(state) + : parseNominalPattern(state) + fieldChildren = Object.freeze([...fieldChildren, ...colon.elements, nested.node]) + state = nested.state + } else { + const binding = expect(state, 'Identifier', [ + 'Comma', + 'RightBrace', + 'IfKeyword', + 'FatArrow', + ]) + fieldChildren = Object.freeze([...fieldChildren, ...colon.elements, ...binding.elements]) + state = binding.state + } + } + children = Object.freeze([...children, syntaxNode(state, 'PatternField', fieldChildren)]) + } + if (nextSignificantKind(state) === 'RightBrace') break + const comma = expect(state, 'Comma', [ + 'Identifier', + 'DotDot', + 'RightBrace', + 'IfKeyword', + 'FatArrow', + ]) + children = Object.freeze([...children, ...comma.elements]) + state = comma.state + } + const right = expect(state, 'RightBrace', ['IfKeyword', 'FatArrow', 'Identifier', 'RightBrace']) + return Object.freeze({ + state: right.state, + node: syntaxNode(right.state, 'UnionVariantPattern', [...children, ...right.elements]), + }) +} + export function parseNominalPattern(initial: State): NodeResult { const target = parseTypePrimary(initial, ['LeftBrace', 'IfKeyword', 'FatArrow', 'RightBrace']) // `Member name` binds the whole member value instead of destructuring its fields. @@ -900,6 +1119,7 @@ export const reservedTemplateBoundaries: ReadonlyArray = Object 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'FnKeyword', 'ImportKeyword', 'EndOfFile', @@ -987,6 +1207,8 @@ export function parsePrimaryExpression( if (kind === 'Call') return parseCallExpression(initial, reservedForEnclosingCalls) if (kind === 'StructLiteral') return parseStructLiteralExpression(initial, reservedForEnclosingCalls) + if (kind === 'UnionVariant') + return parseUnionVariantExpression(initial, reservedForEnclosingCalls) if (kind === 'ArrayLiteral') return parseArrayLiteralExpression(initial, reservedForEnclosingCalls) if (kind === 'Match') return parseMatchExpression(initial, reservedForEnclosingCalls) diff --git a/packages/compiler/src/Parser/Grammar.ts b/packages/compiler/src/Parser/Grammar.ts index 3dd546208..9d3edb609 100644 --- a/packages/compiler/src/Parser/Grammar.ts +++ b/packages/compiler/src/Parser/Grammar.ts @@ -22,6 +22,7 @@ export const expressionFollowing: ReadonlyArray = Object.freeze 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'FnKeyword', 'EffectKeyword', 'ImportKeyword', @@ -70,6 +71,7 @@ export const topLevelFollowing: ReadonlyArray = Object.freeze([ 'ConstKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'ServiceKeyword', 'InterfaceKeyword', 'RoleKeyword', diff --git a/packages/compiler/src/Parser/Statement.ts b/packages/compiler/src/Parser/Statement.ts index d599ea25d..63f54de06 100644 --- a/packages/compiler/src/Parser/Statement.ts +++ b/packages/compiler/src/Parser/Statement.ts @@ -298,6 +298,7 @@ export const endsBlock = (state: State): boolean => { kind === 'ConstKeyword' || kind === 'StructKeyword' || kind === 'EnumKeyword' || + kind === 'UnionKeyword' || kind === 'FnKeyword' || kind === 'ImplKeyword' || (kind === 'EffectKeyword' && peek(state, 1) === 'FnKeyword') @@ -440,6 +441,7 @@ export function parseBlock( 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'FnKeyword', 'EffectKeyword', 'ImportKeyword', diff --git a/packages/compiler/src/Parser/Type.ts b/packages/compiler/src/Parser/Type.ts index dde49c03c..75d864f1d 100644 --- a/packages/compiler/src/Parser/Type.ts +++ b/packages/compiler/src/Parser/Type.ts @@ -622,6 +622,7 @@ export const parseParameterList = (initial: State): NodeResult => { kind !== 'PubKeyword' && kind !== 'StructKeyword' && kind !== 'EnumKeyword' && + kind !== 'UnionKeyword' && kind !== 'ServiceKeyword' && kind !== 'FnKeyword' && kind !== 'EffectKeyword' && @@ -657,6 +658,7 @@ export const parseParameterList = (initial: State): NodeResult => { kind === 'PubKeyword' || kind === 'StructKeyword' || kind === 'EnumKeyword' || + kind === 'UnionKeyword' || kind === 'ServiceKeyword' || kind === 'FnKeyword' || kind === 'EffectKeyword' || @@ -673,6 +675,7 @@ export const parseParameterList = (initial: State): NodeResult => { 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'ServiceKeyword', 'FnKeyword', 'EffectKeyword', @@ -689,6 +692,7 @@ export const parseParameterList = (initial: State): NodeResult => { 'PubKeyword', 'StructKeyword', 'EnumKeyword', + 'UnionKeyword', 'ServiceKeyword', 'FnKeyword', 'EffectKeyword', diff --git a/packages/compiler/src/SyntaxFormatter.ts b/packages/compiler/src/SyntaxFormatter.ts index 3aa0bd58a..710d2eec4 100644 --- a/packages/compiler/src/SyntaxFormatter.ts +++ b/packages/compiler/src/SyntaxFormatter.ts @@ -400,6 +400,76 @@ const printEnumDeclaration = ( ) } +const printUnionVariant = ( + context: Context, + node: SyntaxTree.Node, + prefix: FormatDocument.Document, +): FormatDocument.Document => { + const fields = directNodes(node).filter((child) => child.kind === 'UnionVariantField') + const name = printToken(context, tokenOf(node, 'Identifier'), prefix, true) + const open = directTokens(node).find((token) => token.kind === 'LeftBrace') + if (open === undefined) return name + return FormatDocument.concat( + name, + printDelimited( + context, + open, + fields, + commaTokens(node), + tokenOf(node, 'RightBrace'), + FormatDocument.text(' '), + ), + ) +} + +const printUnionDeclaration = ( + context: Context, + node: SyntaxTree.Node, + prefix: FormatDocument.Document, +): FormatDocument.Document => { + const variants = directNodes(node).filter((child) => child.kind === 'UnionVariant') + const typeParameters = directNodes(node).find((child) => child.kind === 'TypeParameterList') + const publicKeyword = directTokens(node).find((token) => token.kind === 'PubKeyword') + const head = FormatDocument.concat( + ...(publicKeyword === undefined + ? [] + : [printToken(context, publicKeyword, prefix), FormatDocument.text(' ')]), + printToken( + context, + tokenOf(node, 'UnionKeyword'), + publicKeyword === undefined ? prefix : FormatDocument.empty, + ), + printToken(context, tokenOf(node, 'Identifier'), FormatDocument.text(' ')), + ...(typeParameters === undefined ? [] : [printNode(context, typeParameters)]), + ) + const open = tokenOf(node, 'LeftBrace') + const close = tokenOf(node, 'RightBrace') + if (variants.length === 0) { + return FormatDocument.concat( + head, + printToken(context, open, FormatDocument.text(' ')), + printToken(context, close), + ) + } + const commas = commaTokens(node) + return FormatDocument.concat( + head, + printToken(context, open, FormatDocument.text(' ')), + FormatDocument.indent( + FormatDocument.concat( + ...variants.flatMap((variant, index) => { + const comma = commas.at(index) + return [ + printUnionVariant(context, variant, FormatDocument.hardLine), + comma === undefined ? FormatDocument.text(',') : printToken(context, comma), + ] + }), + ), + ), + printToken(context, close, FormatDocument.hardLine), + ) +} + const printServiceOperation = ( context: Context, node: SyntaxTree.Node, @@ -803,6 +873,8 @@ const printNode = ( return printStructDeclaration(context, node, prefix) case 'EnumDeclaration': return printEnumDeclaration(context, node, prefix) + case 'UnionDeclaration': + return printUnionDeclaration(context, node, prefix) case 'ServiceDeclaration': case 'InterfaceDeclaration': return printServiceDeclaration(context, node, prefix) @@ -859,6 +931,70 @@ const printNode = ( ), ) } + case 'UnionVariant': + return printUnionVariant(context, node, prefix) + case 'UnionVariantField': { + const publicKeyword = directTokens(node).find((token) => token.kind === 'PubKeyword') + return FormatDocument.concat( + ...(publicKeyword === undefined + ? [] + : [printToken(context, publicKeyword, prefix, preserveBlank), FormatDocument.text(' ')]), + printToken( + context, + tokenOf(node, 'Identifier'), + publicKeyword === undefined ? prefix : FormatDocument.empty, + preserveBlank, + ), + printToken(context, tokenOf(node, 'Colon')), + printNode( + context, + directNodes(node).at(-1) ?? nodeOf(node, 'TypePath'), + FormatDocument.text(' '), + ), + ) + } + case 'UnionVariantSelector': { + const parent = directNodes(node)[0] ?? nodeOf(node, 'AppliedType') + return FormatDocument.concat( + printNode(context, parent, prefix, preserveBlank), + printToken(context, tokenOf(node, 'Dot')), + printToken(context, tokenOf(node, 'Identifier')), + ) + } + case 'UnionVariantExpression': { + const nodes = directNodes(node) + const selector = nodes[0] ?? nodeOf(node, 'UnionVariantSelector') + const open = directTokens(node).find((token) => token.kind === 'LeftBrace') + if (open === undefined) return printNode(context, selector, prefix, preserveBlank) + return FormatDocument.concat( + printNode(context, selector, prefix, preserveBlank), + printDelimited( + context, + open, + nodes.slice(1), + commaTokens(node), + tokenOf(node, 'RightBrace'), + FormatDocument.text(' '), + ), + ) + } + case 'UnionVariantPattern': { + const nodes = directNodes(node) + const selector = nodes[0] ?? nodeOf(node, 'UnionVariantSelector') + const open = directTokens(node).find((token) => token.kind === 'LeftBrace') + if (open === undefined) return printNode(context, selector, prefix, preserveBlank) + return FormatDocument.concat( + printNode(context, selector, prefix, preserveBlank), + printDelimited( + context, + open, + nodes.slice(1), + commaTokens(node), + tokenOf(node, 'RightBrace'), + FormatDocument.text(' '), + ), + ) + } case 'EnumMember': { const discriminant = directNodes(node).find( (child) => child.kind === 'IntegerLiteralExpression', @@ -1371,7 +1507,10 @@ const printNode = ( case 'PatternField': { const identifiers = directTokens(node).filter((token) => token.kind === 'Identifier') const nested = directNodes(node).find( - (child) => child.kind === 'NominalPattern' || child.kind === 'BindingPattern', + (child) => + child.kind === 'NominalPattern' || + child.kind === 'UnionVariantPattern' || + child.kind === 'BindingPattern', ) const colon = directTokens(node).find((token) => token.kind === 'Colon') const name = identifiers[0] ?? tokenOf(node, 'Identifier') diff --git a/packages/compiler/src/SyntaxTree.ts b/packages/compiler/src/SyntaxTree.ts index e5ed93d76..e6ab8a56e 100644 --- a/packages/compiler/src/SyntaxTree.ts +++ b/packages/compiler/src/SyntaxTree.ts @@ -14,6 +14,12 @@ export type NodeKind = | 'StructDeclaration' | 'EnumDeclaration' | 'EnumMember' + | 'UnionDeclaration' + | 'UnionVariant' + | 'UnionVariantField' + | 'UnionVariantSelector' + | 'UnionVariantExpression' + | 'UnionVariantPattern' | 'ServiceDeclaration' | 'InterfaceDeclaration' | 'RoleDeclaration' diff --git a/packages/compiler/src/Token.ts b/packages/compiler/src/Token.ts index 1e6f62a7d..de6bd1a74 100644 --- a/packages/compiler/src/Token.ts +++ b/packages/compiler/src/Token.ts @@ -16,6 +16,7 @@ export type TokenKind = | 'PubKeyword' | 'StructKeyword' | 'EnumKeyword' + | 'UnionKeyword' | 'ServiceKeyword' | 'InterfaceKeyword' | 'RoleKeyword' @@ -96,6 +97,7 @@ const descriptions: Readonly> = Object.freeze({ PubKeyword: '`pub`', StructKeyword: '`struct`', EnumKeyword: '`enum`', + UnionKeyword: '`union`', ServiceKeyword: '`service`', InterfaceKeyword: '`interface`', RoleKeyword: '`role`', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 8953672c8..ca344f2cb 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '53b2a4dfb9bd039b5b4df5fd758bac8a2000929c0ce85cbc6592730f4500a023' +export const compilerDigest = '314c09c33a1b2a2bb9c213a907a390980b69f4ac3a10c235ffb91413f2b99ca9' diff --git a/packages/compiler/test/Lexer.test.ts b/packages/compiler/test/Lexer.test.ts index 434e3fcd9..d253db852 100644 --- a/packages/compiler/test/Lexer.test.ts +++ b/packages/compiler/test/Lexer.test.ts @@ -524,6 +524,17 @@ it('recognizes struct only as a complete keyword', () => { ) }) +it('recognizes union only as a complete keyword', () => { + const result = Lexer.lex( + SourceFile.make('memory://union-keyword.silk', ascii('union unionize unions')), + ) + + assert.deepEqual( + result.tokens.filter((token) => token.kind !== 'Whitespace').map((token) => token.kind), + ['UnionKeyword', 'Identifier', 'Identifier', 'EndOfFile'], + ) +}) + it('recognizes the effect execution and failure keywords without prefix capture', () => { const result = Lexer.lex( SourceFile.make( diff --git a/packages/compiler/test/LexerPressure.test.ts b/packages/compiler/test/LexerPressure.test.ts index f7d78669d..1b0c69a9f 100644 --- a/packages/compiler/test/LexerPressure.test.ts +++ b/packages/compiler/test/LexerPressure.test.ts @@ -99,6 +99,7 @@ const tokenKinds = [ 'CharLiteral', 'RoleKeyword', 'EnumKeyword', + 'UnionKeyword', ] as const satisfies ReadonlyArray const tokenCode: Readonly> = Object.freeze({ @@ -180,6 +181,7 @@ const tokenCode: Readonly> = Object.freeze({ CharLiteral: 75, RoleKeyword: 76, EnumKeyword: 77, + UnionKeyword: 78, }) interface ExpectedToken { diff --git a/packages/compiler/test/Parser.test.ts b/packages/compiler/test/Parser.test.ts index 054ed5c49..de9493f94 100644 --- a/packages/compiler/test/Parser.test.ts +++ b/packages/compiler/test/Parser.test.ts @@ -2486,6 +2486,62 @@ pub enum(u8) ExitCode { assert.deepEqual(reconstructedBytes(result), ascii(source)) }) +it('parses generic nominal unions, applied constructors, and patterns losslessly', () => { + const source = `pub union Result { + Success { pub value: A }, + Failure { pub error: E }, +} +pub fn inspect(result: Result) -> i32 { + let fallback = Result.Failure { error: true } + return match move result { + Result.Success { value } => value + Result.Failure { error: _ } => 0 + } +}` + const result = parseText('memory/nominal-union', source) + const declaration = SyntaxTree.directNode(result.root, 'UnionDeclaration') + const variants = + declaration === undefined ? [] : SyntaxTree.directNodes(declaration, 'UnionVariant') + const fields = variants.flatMap((variant) => SyntaxTree.directNodes(variant, 'UnionVariantField')) + const constructors = descendants(result.root).filter( + (element): element is SyntaxTree.Node => + SyntaxTree.isNode(element) && element.kind === 'UnionVariantExpression', + ) + const patterns = descendants(result.root).filter( + (element): element is SyntaxTree.Node => + SyntaxTree.isNode(element) && element.kind === 'UnionVariantPattern', + ) + + assert.strictEqual(variants.length, 2) + assert.strictEqual(fields.length, 2) + assert.strictEqual(constructors.length, 1) + assert.strictEqual(patterns.length, 2) + assert.deepEqual(result.parserDiagnostics, []) + assertOriginalTokenTraversal(result) + assert.deepEqual(reconstructedBytes(result), ascii(source)) +}) + +it('rejects empty field variants and recovers at sibling variants and declarations', () => { + const source = + 'union Broken { Empty {}, Good, Bad { value: }, Last { value: i32 } } pub fn after() -> i32 { return 1 }' + const result = parseText('memory/damaged-nominal-union', source) + const declaration = SyntaxTree.directNode(result.root, 'UnionDeclaration') + const variants = + declaration === undefined ? [] : SyntaxTree.directNodes(declaration, 'UnionVariant') + + assert.deepEqual( + variants.map((variant) => directTokenText(result, variant, 'Identifier')), + ['Empty', 'Good', 'Bad', 'Last'], + ) + assert.strictEqual(missingLeaves(variants[0] ?? result.root).length > 0, true) + assert.strictEqual(missingLeaves(variants[2] ?? result.root).length > 0, true) + assert.deepEqual(missingLeaves(variants[1] ?? result.root), []) + assert.deepEqual(missingLeaves(variants[3] ?? result.root), []) + assert.notStrictEqual(SyntaxTree.directNode(result.root, 'FunctionDeclaration'), undefined) + assertOriginalTokenTraversal(result) + assert.deepEqual(reconstructedBytes(result), ascii(source)) +}) + it('recovers damaged enum members before the following declaration', () => { const source = 'enum Broken { Pass Fail = } pub fn after() -> i32 { return 1 }' const result = parseText('memory/damaged-enum', source) diff --git a/packages/compiler/test/SyntaxFormatter.test.ts b/packages/compiler/test/SyntaxFormatter.test.ts index 78f2caa48..6aa87b9e5 100644 --- a/packages/compiler/test/SyntaxFormatter.test.ts +++ b/packages/compiler/test/SyntaxFormatter.test.ts @@ -78,6 +78,7 @@ const nodeKinds = (node: SyntaxTree.Node): ReadonlyArray => ] const completeNodeKinds: ReadonlyArray = Object.freeze([ + 'AppliedType', 'ArgumentList', 'ArrayLiteralExpression', 'AssignmentStatement', @@ -98,6 +99,12 @@ const completeNodeKinds: ReadonlyArray = Object.freeze([ 'EffectExpression', 'EnumDeclaration', 'EnumMember', + 'UnionDeclaration', + 'UnionVariant', + 'UnionVariantField', + 'UnionVariantSelector', + 'UnionVariantExpression', + 'UnionVariantPattern', 'FieldProjectionExpression', 'FailStatement', 'FailureRow', @@ -141,6 +148,9 @@ const completeNodeKinds: ReadonlyArray = Object.freeze([ 'StructField', 'StructFieldInitializer', 'StructLiteralExpression', + 'TypeArgumentList', + 'TypeParameter', + 'TypeParameterList', 'TypePath', 'UnionType', 'UnitExpression', @@ -198,6 +208,56 @@ pub enum(u8) ExitCode { }), ) +it.effect('formats nominal unions, constructors, and patterns canonically and idempotently', () => + Effect.gen(function* () { + const source = + 'pub union Result{Success{pub value:A},Failure{pub error:E}} fn inspect(value:Result)->i32{return match move value{Result.Success{value}=>value Result.Failure{error:_}=>0}}' + const first = yield* SyntaxFormatter.format(parse('memory://nominal-union-format.silk', source)) + const text = formattedText(first) + assert.strictEqual( + text, + `pub union Result { + Success {pub value: A}, + Failure {pub error: E}, +} + +fn inspect(value: Result) -> i32 { + return match move value { + Result.Success {value} => value + Result.Failure {error: _} => 0 + } +} +`, + ) + const second = yield* SyntaxFormatter.format(parse('memory://nominal-union-format.silk', text)) + assert.strictEqual(formattedText(second), text) + assert.strictEqual(second.changed, false) + }), +) + +it.effect('preserves nominal union comments idempotently', () => + Effect.gen(function* () { + const source = `// union docs +pub union Maybe { + // unit + None, + // payload + Some { pub value: T }, +}` + const first = yield* SyntaxFormatter.format( + parse('memory://nominal-union-comments.silk', source), + ) + const text = formattedText(first) + assert.strictEqual(text.includes('// union docs'), true) + assert.strictEqual(text.includes('// unit'), true) + assert.strictEqual(text.includes('// payload'), true) + const second = yield* SyntaxFormatter.format( + parse('memory://nominal-union-comments.silk', text), + ) + assert.strictEqual(formattedText(second), text) + }), +) + it.effect('omits semantic fallthrough completion nodes from formatted source', () => Effect.gen(function* () { const source = 'fn missing()->i32 { let value=42 } pub fn main()->() {}' @@ -935,6 +995,7 @@ pub struct Token { span: Span } pub struct End {} enum AssertionResult { Pass, Fail, Skip } pub enum(u8) ExitCode { Success = 0, Failure = 1 } +pub union Maybe { None, Some { pub value: T } } pub role Clock fn helper(value: i32, other: i32) -> i32 { let mut moved = move value @@ -974,6 +1035,13 @@ fn execute(problem: Token, borrowed: &End) -> i32 { fn selected() -> typeof(helper) { return helper } +fn selectMaybe(value: Maybe) -> Maybe { + let fallback = Maybe.None + return match move value { + Maybe.Some { value } => Maybe.Some { value: value } + Maybe.None => move fallback + } +} fn borrow(values: [i32; 2], output: [i32; 2]) -> i32 { let mut target = move output return scan(&values, &mut target) diff --git a/packages/compiler/test/goldens/canonical.silk b/packages/compiler/test/goldens/canonical.silk index 6a3260aa3..5fe224102 100644 --- a/packages/compiler/test/goldens/canonical.silk +++ b/packages/compiler/test/goldens/canonical.silk @@ -30,6 +30,11 @@ pub enum(u8) ExitCode { Failure = 1, } +pub union Maybe { + None, + Some {pub value: T}, +} + pub role Clock fn helper(value: i32, other: i32) -> i32 { @@ -108,6 +113,14 @@ fn selected() -> typeof(helper) { return helper } +fn selectMaybe(value: Maybe) -> Maybe { + let fallback = Maybe.None + return match move value { + Maybe.Some {value} => Maybe.Some {value: value} + Maybe.None => move fallback + } +} + fn borrow(values: [i32; 2], output: [i32; 2]) -> i32 { let mut target = move output return scan(&values, &mut target) diff --git a/packages/editor-support/src/CodeMirror.ts b/packages/editor-support/src/CodeMirror.ts index 66f40e513..25a888e20 100644 --- a/packages/editor-support/src/CodeMirror.ts +++ b/packages/editor-support/src/CodeMirror.ts @@ -41,6 +41,7 @@ const categories: Record = { PubKeyword: keyword, StructKeyword: keyword, EnumKeyword: keyword, + UnionKeyword: keyword, ServiceKeyword: keyword, InterfaceKeyword: keyword, RoleKeyword: keyword, diff --git a/packages/editor-support/src/TextMate.ts b/packages/editor-support/src/TextMate.ts index a3c3ddb98..d02c66947 100644 --- a/packages/editor-support/src/TextMate.ts +++ b/packages/editor-support/src/TextMate.ts @@ -12,6 +12,7 @@ export const keywords: Record = { PubKeyword: 'pub', StructKeyword: 'struct', EnumKeyword: 'enum', + UnionKeyword: 'union', ServiceKeyword: 'service', InterfaceKeyword: 'interface', RoleKeyword: 'role', From 6ba66faffcc455fcfd212f6476a0c76b33e7a688 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 13:06:56 -0300 Subject: [PATCH 03/42] feat(compiler): index nominal union declarations --- apps/docs/content/language/diagnostics.md | 7 +- openspec/changes/add-nominal-unions/tasks.md | 6 +- packages/compiler/src/Analysis.ts | 14 +- packages/compiler/src/BootstrapEvaluation.ts | 15 +- packages/compiler/src/BootstrapPlace.ts | 10 +- packages/compiler/src/BootstrapStorage.ts | 9 +- packages/compiler/src/CleanupPlan.ts | 6 +- packages/compiler/src/Completion.ts | 3 + .../compiler/src/DeclarationCollection.ts | 249 +++++++++++++++--- .../compiler/src/DeclarationCompletion.ts | 107 ++++++-- packages/compiler/src/DeclarationFacts.ts | 146 +++++++++- packages/compiler/src/Diagnostic.ts | 59 +++++ packages/compiler/src/ExpressionAnalysis.ts | 14 +- .../compiler/src/InspectorProjectBackend.ts | 2 + packages/compiler/src/Layout.ts | 13 +- packages/compiler/src/LayoutEncode.ts | 3 +- packages/compiler/src/LayoutVerify.ts | 6 +- packages/compiler/src/LowerExpression.ts | 8 +- packages/compiler/src/MirVerification.ts | 20 +- packages/compiler/src/ModuleSurface.ts | 21 ++ packages/compiler/src/NameResolution.ts | 2 + packages/compiler/src/NativeAggregate.ts | 6 +- packages/compiler/src/NativePlaceOperation.ts | 20 +- packages/compiler/src/Parser/Expression.ts | 19 +- packages/compiler/src/Presentation.ts | 16 ++ packages/compiler/src/SemanticOccurrence.ts | 28 +- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 39 +-- .../compiler/test/DeclarationIndex.test.ts | 77 ++++++ 29 files changed, 717 insertions(+), 210 deletions(-) diff --git a/apps/docs/content/language/diagnostics.md b/apps/docs/content/language/diagnostics.md index a9d8ecf10..3168740b3 100644 --- a/apps/docs/content/language/diagnostics.md +++ b/apps/docs/content/language/diagnostics.md @@ -16,11 +16,11 @@ $ pnpm --filter @silklang/compiler documentation:generate | `LEX` | Lexical | 7 | | `PAR` | Parser | 4 | | `MOD` | Module | 3 | -| `SEM` | Semantic | 155 | +| `SEM` | Semantic | 158 | | `OWN` | Ownership | 16 | | `LAY` | Layout | 1 | -There are 186 codes in total. +There are 189 codes in total. ## Lexical (`LEX`) @@ -210,6 +210,9 @@ There are 186 codes in total. | `SEM0161` | Stable code for a scalar enum pattern naming a member of another enum. | `Enum pattern from cannot match ` | | `SEM0162` | Stable code for an integer literal pattern used against a scalar enum. | `Integer pattern cannot match enum ` | | `SEM0163` | Stable code for effect-block return sites whose success types disagree. | `Effect block return sites have incompatible types: ` | +| `SEM0164` | Stable code for a nominal union declaration with no variants. | `Union must declare at least one variant` | +| `SEM0165` | Stable code for a repeated variant name within one nominal union. | `Duplicate union variant ` | +| `SEM0166` | Stable code for a named-field variant whose braces contain no field. | `Union variant must omit braces or declare at least one field` | ## Ownership (`OWN`) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index c9a3db812..83714093d 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -5,12 +5,12 @@ - [x] 1.3 Implement parser entry points for generic union declarations, constructor qualifiers with explicit argument prefixes, and fully applied variant patterns, and verify focused parser tests cover valid mixed variants and reject empty named-field bodies. - [x] 1.4 Implement variant-local parser recovery for missing names, types, separators, and braces, and verify damaged-variant tests preserve valid siblings and following declarations. - [x] 1.5 Extend the formatter, syntax correspondence, and source presentation for union declarations, constructors, and patterns, and verify formatting is idempotent and preserves comments. -- [ ] 1.6 Add stable structured diagnostic catalog entries for union-specific syntax and semantic failures, regenerate catalog artifacts, and verify diagnostic tests assert codes, spans, related spans, and details rather than message text. +- [x] 1.6 Add stable structured diagnostic catalog entries for union-specific syntax and semantic failures, regenerate catalog artifacts, and verify diagnostic tests assert codes, spans, related spans, and details rather than message text. ## 2. Canonical Declarations and Module Surfaces -- [ ] 2.1 Add canonical `UnionFact`, subordinate variant identities, and variant-scoped field ownership while generalizing shared field facts away from struct-only owners, and verify identity tests distinguish same-spelled variants and fields under different parents. -- [ ] 2.2 Collect unions in the ordinary cross-kind module namespace with parent parameters and source-ordered variants before bodies, and verify forward declarations, duplicates, empty unions, and cross-kind collisions in declaration-index tests. +- [x] 2.1 Add canonical `UnionFact`, subordinate variant identities, and variant-scoped field ownership while generalizing shared field facts away from struct-only owners, and verify identity tests distinguish same-spelled variants and fields under different parents. +- [x] 2.2 Collect unions in the ordinary cross-kind module namespace with parent parameters and source-ordered variants before bodies, and verify forward declarations, duplicates, empty unions, and cross-kind collisions in declaration-index tests. - [ ] 2.3 Resolve every variant field type, visibility exposure, generic reference, and inline aggregate dependency before body analysis, and verify invalid fields preserve sibling facts while making the complete parent non-executable. - [ ] 2.4 Encode union declarations in deterministic module semantic surfaces, and verify encode/decode, equality, and dependency invalidation respond to variant order, kind, field, type, visibility, bound, and availability changes but ignore body-only edits. - [ ] 2.5 Extend semantic occurrence, navigation, completion, documentation, and Analysis facade queries for parent, variant, and field identities, and verify go-to-definition/reference tests use canonical facts rather than syntax reconstruction. diff --git a/packages/compiler/src/Analysis.ts b/packages/compiler/src/Analysis.ts index b4133d08b..14a2de5dc 100644 --- a/packages/compiler/src/Analysis.ts +++ b/packages/compiler/src/Analysis.ts @@ -394,11 +394,8 @@ const syntaxForIdentity = ( if (identity._tag === 'FieldIdentity') { for (const headers of self.index.modules) for (const declaration of headers.structs) { - const field = declaration.fields.find( - (candidate) => - candidate.id.struct.sourceId === identity.id.struct.sourceId && - candidate.id.struct.ordinal === identity.id.struct.ordinal && - candidate.id.ordinal === identity.id.ordinal, + const field = declaration.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, identity.id), ) if (field !== undefined) return field.syntax } @@ -540,11 +537,8 @@ const presentationOfIdentity = ( if (identity._tag === 'FieldIdentity') { for (const headers of self.index.modules) for (const declaration of headers.structs) { - const field = declaration.fields.find( - (candidate) => - candidate.id.struct.sourceId === identity.id.struct.sourceId && - candidate.id.struct.ordinal === identity.id.struct.ordinal && - candidate.id.ordinal === identity.id.ordinal, + const field = declaration.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, identity.id), ) if (field !== undefined) return hoverPresentation(Presentation.field(field), declaredType(field.declaredType)) diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index 1fe9cce8b..6fd688cf8 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -16,6 +16,7 @@ import * as BootstrapPlace from './BootstrapPlace.js' import * as BootstrapStorage from './BootstrapStorage.js' import type * as ChildProcess from './ChildProcess.js' import type * as CleanupPlan from './CleanupPlan.js' +import * as DeclarationFacts from './DeclarationFacts.js' import * as ExecutionTransition from './ExecutionTransition.js' import * as FloatingPoint from './FloatingPoint.js' import type * as Hir from './Hir.js' @@ -3963,11 +3964,8 @@ function* executeFunction( if (aggregate._tag !== 'AggregateValue') { throw new RangeError('MIR verifier allowed projection from a scalar value') } - const selected = aggregate.fields.find( - (candidate) => - candidate.field.ordinal === operation.field.ordinal && - candidate.field.struct.sourceId === operation.field.struct.sourceId && - candidate.field.struct.ordinal === operation.field.struct.ordinal, + const selected = aggregate.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.field, operation.field), ) if (selected === undefined) { throw new RangeError('MIR verifier allowed projection of a missing aggregate field') @@ -4004,11 +4002,8 @@ function* executeFunction( 'MIR verifier allowed a field selector on a non-struct value', ) } - const field = selected.fields.find( - (candidate) => - candidate.field.ordinal === selector.field.ordinal && - candidate.field.struct.sourceId === selector.field.struct.sourceId && - candidate.field.struct.ordinal === selector.field.struct.ordinal, + const field = selected.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.field, selector.field), ) if (field === undefined) { throw new RangeError('MIR verifier allowed a missing field selector') diff --git a/packages/compiler/src/BootstrapPlace.ts b/packages/compiler/src/BootstrapPlace.ts index 85dd0be81..af2501951 100644 --- a/packages/compiler/src/BootstrapPlace.ts +++ b/packages/compiler/src/BootstrapPlace.ts @@ -1,4 +1,5 @@ import type { SliceValue, Value } from './BootstrapValue.js' +import * as DeclarationFacts from './DeclarationFacts.js' import type * as Mir from './Mir.js' export interface Access { @@ -33,11 +34,8 @@ export const walkPlace = ( if (selector._tag === 'FieldSelector') { if (selected._tag !== 'AggregateValue') throw new RangeError('MIR verifier allowed a field selector on a non-struct value') - const field = selected.fields.find( - (candidate) => - candidate.field.ordinal === selector.field.ordinal && - candidate.field.struct.sourceId === selector.field.struct.sourceId && - candidate.field.struct.ordinal === selector.field.struct.ordinal, + const field = selected.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.field, selector.field), ) if (field === undefined) throw new RangeError('MIR verifier allowed a missing field selector') selected = field.value @@ -171,7 +169,7 @@ export const selectorKey = (selectors: ReadonlyArray): string selectors .map((selector) => { if (selector._tag === 'FieldSelector') { - return `field:${selector.field.struct.sourceId}:${selector.field.struct.ordinal}:${selector.field.ordinal}` + return `field:${DeclarationFacts.fieldIdKey(selector.field)}` } if (selector._tag === 'SliceElementSelector') { return `slice:${selector.index.ordinal}` diff --git a/packages/compiler/src/BootstrapStorage.ts b/packages/compiler/src/BootstrapStorage.ts index f53417ed4..1a15a47ef 100644 --- a/packages/compiler/src/BootstrapStorage.ts +++ b/packages/compiler/src/BootstrapStorage.ts @@ -1,6 +1,6 @@ import type { AggregateValue, Value } from './BootstrapValue.js' import type * as CleanupPlan from './CleanupPlan.js' -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import type * as ExecutionPackage from './ExecutionPackage.js' import * as Type from './Type.js' import * as WakeCell from './WakeCell.js' @@ -259,11 +259,8 @@ export const selectFieldPath = ( for (const selector of path) { if (selected._tag !== 'AggregateValue') throw new RangeError('MIR verifier allowed a match field below a non-struct value') - const field: AggregateValue['fields'][number] | undefined = selected.fields.find( - (candidate) => - candidate.field.ordinal === selector.ordinal && - candidate.field.struct.sourceId === selector.struct.sourceId && - candidate.field.struct.ordinal === selector.struct.ordinal, + const field: AggregateValue['fields'][number] | undefined = selected.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.field, selector), ) if (field === undefined) throw new RangeError('MIR verifier allowed a missing match field') selected = field.value diff --git a/packages/compiler/src/CleanupPlan.ts b/packages/compiler/src/CleanupPlan.ts index 8c06358ae..82e89c66f 100644 --- a/packages/compiler/src/CleanupPlan.ts +++ b/packages/compiler/src/CleanupPlan.ts @@ -337,10 +337,8 @@ export const cleanupTypeAtPath = ( current.arguments, ) if (substitution === undefined) return undefined - const field = declaration.fields.find( - (candidate) => - candidate.id.struct.ordinal === fieldId.struct.ordinal && - candidate.id.ordinal === fieldId.ordinal, + const field = declaration.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, fieldId), ) current = field?.declaredType._tag === 'Resolved' diff --git a/packages/compiler/src/Completion.ts b/packages/compiler/src/Completion.ts index ade09c481..d580a63d0 100644 --- a/packages/compiler/src/Completion.ts +++ b/packages/compiler/src/Completion.ts @@ -105,6 +105,7 @@ const declarationKind = ( case 'InterfaceDeclaration': case 'RoleDeclaration': case 'EnumDeclaration': + case 'UnionDeclaration': return 'Type' case 'StructDeclaration': return aggregateKind @@ -126,6 +127,8 @@ const declarationDetail = (declaration: DeclarationFacts.MemberFact): Presentati return PresentationRenderer.enumDeclaration(declaration) case 'StructDeclaration': return PresentationRenderer.structDeclaration(declaration) + case 'UnionDeclaration': + return PresentationRenderer.unionDeclaration(declaration) } } diff --git a/packages/compiler/src/DeclarationCollection.ts b/packages/compiler/src/DeclarationCollection.ts index e158c4ad2..fd51ad7e8 100644 --- a/packages/compiler/src/DeclarationCollection.ts +++ b/packages/compiler/src/DeclarationCollection.ts @@ -5,6 +5,7 @@ import type { CanonicalEnumMemberId, CanonicalId, CanonicalState, + CanonicalUnionVariantId, ConformanceFact, ConformanceRequirementFact, ConstantFact, @@ -22,6 +23,7 @@ import type { FailureRowFact, FieldFact, FieldId, + FieldOwnerId, FieldState, InterfaceFact, MemberFact, @@ -40,6 +42,9 @@ import type { TypeParameterFact, TypePathFact, TypeResolution, + UnionFact, + UnionVariantFact, + UnionVariantId, } from './DeclarationFacts.js' import { enumValueOperation, @@ -1105,50 +1110,49 @@ const duplicateParameterDiagnostics = (parameters: ReadonlyArray) const collectFields = ( source: SourceFile.SourceFile, node: SyntaxTree.Node, - structId: DeclarationId, + owner: FieldOwnerId, + nodeKind: 'StructField' | 'UnionVariantField', typeParameters: ReadonlyMap, ) => { const first = new Map() const diagnostics: Array = [] - const fields = SyntaxTree.directNodes(node, 'StructField').map( - (fieldNode, ordinal): FieldFact => { - const id: FieldId = Object.freeze({ _tag: 'FieldId', struct: structId, ordinal }) - const name = presentName(source, fieldNode) - const type = analyzeDeclaredType(source, declaredTypeNode(fieldNode), typeParameters) - diagnostics.push(...type.diagnostics) - let state: FieldState - if (name._tag !== 'Present') state = Object.freeze({ _tag: 'Unidentified' }) - else { - const original = first.get(name.spelling) - if (original === undefined) { - first.set(name.spelling, Object.freeze({ id, token: name.token })) - state = Object.freeze({ _tag: 'Unique', id }) - } else { - const diagnostic = Diagnostic.duplicateFieldName( - name.spelling, - original.token.span, - name.token.span, - ) - diagnostics.push(diagnostic) - state = Object.freeze({ - _tag: 'Duplicate', - original: original.id, - cause: Diagnostic.identity(diagnostic), - }) - } + const fields = SyntaxTree.directNodes(node, nodeKind).map((fieldNode, ordinal): FieldFact => { + const id: FieldId = Object.freeze({ _tag: 'FieldId', owner, ordinal }) + const name = presentName(source, fieldNode) + const type = analyzeDeclaredType(source, declaredTypeNode(fieldNode), typeParameters) + diagnostics.push(...type.diagnostics) + let state: FieldState + if (name._tag !== 'Present') state = Object.freeze({ _tag: 'Unidentified' }) + else { + const original = first.get(name.spelling) + if (original === undefined) { + first.set(name.spelling, Object.freeze({ id, token: name.token })) + state = Object.freeze({ _tag: 'Unique', id }) + } else { + const diagnostic = Diagnostic.duplicateFieldName( + name.spelling, + original.token.span, + name.token.span, + ) + diagnostics.push(diagnostic) + state = Object.freeze({ + _tag: 'Duplicate', + original: original.id, + cause: Diagnostic.identity(diagnostic), + }) } - return Object.freeze({ - _tag: 'StructField', - id, - state, - visibility: - SyntaxTree.directToken(fieldNode, 'PubKeyword') === undefined ? 'Private' : 'Public', - name, - declaredType: type.fact, - syntax: fieldNode, - }) - }, - ) + } + return Object.freeze({ + _tag: 'AggregateField', + id, + state, + visibility: + SyntaxTree.directToken(fieldNode, 'PubKeyword') === undefined ? 'Private' : 'Public', + name, + declaredType: type.fact, + syntax: fieldNode, + }) + }) return Object.freeze({ fields: Object.freeze(fields), diagnostics: Object.freeze(diagnostics) }) } @@ -2097,6 +2101,150 @@ const collectEnum = ( }) } +const collectUnion = ( + source: SourceFile.SourceFile, + node: SyntaxTree.Node, + id: DeclarationId, + canonical: CanonicalState, + visibility: 'Private' | 'Public', + name: DeclaredName, + typeParameters: ReturnType, + diagnostics: Array, +): UnionFact => { + const unionDiagnostics: Array = [] + const variantNodes = SyntaxTree.directNodes(node, 'UnionVariant') + if (variantNodes.length === 0) { + unionDiagnostics.push( + Diagnostic.emptyNominalUnion( + name._tag === 'Present' ? name.spelling : '', + tightSpan(node), + ), + ) + } + const first = new Map< + string, + { + readonly id: UnionVariantId + readonly canonical?: CanonicalUnionVariantId + readonly token: Token.Token + } + >() + const variants = variantNodes.map((variantNode, ordinal): UnionVariantFact => { + const variantId: UnionVariantId = Object.freeze({ _tag: 'UnionVariantId', union: id, ordinal }) + const variantName = presentName(source, variantNode) + let variantCanonical: UnionVariantFact['canonical'] = Object.freeze({ + _tag: 'Unidentified', + }) + if (variantName._tag === 'Present') { + const original = first.get(variantName.spelling) + if (original === undefined) { + const canonicalVariant = + canonical._tag === 'Canonical' + ? Object.freeze({ + _tag: 'CanonicalUnionVariantId' as const, + union: canonical.id, + name: variantName.spelling, + }) + : undefined + first.set( + variantName.spelling, + Object.freeze({ + id: variantId, + token: variantName.token, + ...(canonicalVariant === undefined ? {} : { canonical: canonicalVariant }), + }), + ) + if (canonicalVariant !== undefined) { + variantCanonical = Object.freeze({ _tag: 'Canonical', id: canonicalVariant }) + } + } else { + const diagnostic = Diagnostic.duplicateUnionVariant( + variantName.spelling, + original.token.span, + variantName.token.span, + ) + unionDiagnostics.push(diagnostic) + if (original.canonical !== undefined) { + variantCanonical = Object.freeze({ + _tag: 'Duplicate', + original: original.canonical, + cause: Diagnostic.identity(diagnostic), + }) + } + } + } + const hasFieldBody = SyntaxTree.directToken(variantNode, 'LeftBrace') !== undefined + const fieldNodes = SyntaxTree.directNodes(variantNode, 'UnionVariantField') + if ( + hasFieldBody && + fieldNodes.every( + (fieldNode) => + SyntaxTree.tokens(fieldNode).filter( + (token) => + token.kind !== 'Whitespace' && + token.kind !== 'LineComment' && + token.kind !== 'DocComment' && + token.kind !== 'ModuleDocComment', + ).length === 0, + ) + ) { + unionDiagnostics.push( + Diagnostic.emptyUnionVariant( + variantName._tag === 'Present' ? variantName.spelling : '', + tightSpan(variantNode), + ), + ) + } + const collected = collectFields( + source, + variantNode, + Object.freeze({ _tag: 'UnionVariantFieldOwnerId', variant: variantId }), + 'UnionVariantField', + typeParameters.environment, + ) + unionDiagnostics.push(...collected.diagnostics) + return Object.freeze({ + _tag: 'UnionVariant', + id: variantId, + canonical: variantCanonical, + name: variantName, + kind: hasFieldBody ? 'Fields' : 'Unit', + fields: collected.fields, + syntax: variantNode, + }) + }) + diagnostics.push(...unionDiagnostics) + const valid = + unionDiagnostics.length === 0 && + SyntaxTree.isAvailableSyntax(node) && + variants.length > 0 && + variants.every( + (variant) => + variant.name._tag === 'Present' && + variant.canonical._tag === 'Canonical' && + variant.fields.every((field) => field.state._tag === 'Unique'), + ) + return Object.freeze({ + _tag: 'UnionDeclaration', + id, + canonical, + visibility, + typeParameters: typeParameters.facts, + name, + variants: Object.freeze(variants), + dependency: Object.freeze({ _tag: 'Available', types: Object.freeze([]) }), + validity: valid + ? Object.freeze({ _tag: 'Valid' }) + : Object.freeze({ + _tag: 'Invalid', + causes: Object.freeze( + unionDiagnostics.map((diagnostic) => Diagnostic.identity(diagnostic)), + ), + }), + syntax: node, + }) +} + const collectModule = (syntax: SyntaxFile.SyntaxFile): ModuleHeaders => { const source = syntax.source const nodes = syntax.root.children.filter( @@ -2105,6 +2253,7 @@ const collectModule = (syntax: SyntaxFile.SyntaxFile): ModuleHeaders => { (element.kind === 'FunctionDeclaration' || element.kind === 'StructDeclaration' || element.kind === 'EnumDeclaration' || + element.kind === 'UnionDeclaration' || element.kind === 'ServiceDeclaration' || element.kind === 'InterfaceDeclaration' || element.kind === 'RoleDeclaration' || @@ -2382,8 +2531,25 @@ const collectModule = (syntax: SyntaxFile.SyntaxFile): ModuleHeaders => { } if (node.kind === 'EnumDeclaration') return collectEnum(source, node, id, canonical, visibility, name, diagnostics) + if (node.kind === 'UnionDeclaration') + return collectUnion( + source, + node, + id, + canonical, + visibility, + name, + typeParameters, + diagnostics, + ) if (node.kind === 'StructDeclaration') { - const collected = collectFields(source, node, id, typeParameters.environment) + const collected = collectFields( + source, + node, + Object.freeze({ _tag: 'StructFieldOwnerId', declaration: id }), + 'StructField', + typeParameters.environment, + ) diagnostics.push(...collected.diagnostics) return Object.freeze({ _tag: 'StructDeclaration', @@ -2872,6 +3038,9 @@ const collectModule = (syntax: SyntaxFile.SyntaxFile): ModuleHeaders => { enums: Object.freeze( members.filter((member): member is EnumFact => member._tag === 'EnumDeclaration'), ), + unions: Object.freeze( + members.filter((member): member is UnionFact => member._tag === 'UnionDeclaration'), + ), services: Object.freeze( members.filter((member): member is ServiceFact => member._tag === 'ServiceDeclaration'), ), diff --git a/packages/compiler/src/DeclarationCompletion.ts b/packages/compiler/src/DeclarationCompletion.ts index 23cb468ee..ee1da334c 100644 --- a/packages/compiler/src/DeclarationCompletion.ts +++ b/packages/compiler/src/DeclarationCompletion.ts @@ -11,6 +11,7 @@ import type { ServiceFact, StructFact, TypeParameterFact, + UnionFact, } from './DeclarationFacts.js' import { closeConformanceSelf, @@ -283,16 +284,54 @@ export const complete = ( }) } if (member._tag === 'RoleDeclaration' || member._tag === 'EnumDeclaration') return member - const fields = member.fields.map((field) => { - const resolved = resolveDeclaredType( - module.module, - field.declaredType, - resolvers, - self.modules, + const resolveFields = (fields: StructFact['fields']): StructFact['fields'] => + Object.freeze( + fields.map((field) => { + const resolved = resolveDeclaredType( + module.module, + field.declaredType, + resolvers, + self.modules, + ) + diagnostics.push(...resolved.diagnostics) + return Object.freeze({ ...field, declaredType: resolved.fact }) + }), ) - diagnostics.push(...resolved.diagnostics) - return Object.freeze({ ...field, declaredType: resolved.fact }) - }) + if (member._tag === 'UnionDeclaration') { + const variants = Object.freeze( + member.variants.map((variant) => + Object.freeze({ ...variant, fields: resolveFields(variant.fields) }), + ), + ) + const unavailableCauses = variants.flatMap((variant) => + variant.fields.flatMap((field) => + field.declaredType._tag === 'Unresolved' && field.declaredType.cause !== undefined + ? [field.declaredType.cause] + : [], + ), + ) + return Object.freeze({ + ...member, + typeParameters: resolveBounds( + module.module, + member.typeParameters, + resolvers, + self.modules, + diagnostics, + ), + variants, + validity: + member.validity._tag === 'Valid' && unavailableCauses.length === 0 + ? member.validity + : Object.freeze({ + _tag: 'Invalid' as const, + causes: Object.freeze([ + ...(member.validity._tag === 'Invalid' ? member.validity.causes : []), + ...unavailableCauses, + ]), + }), + }) + } return Object.freeze({ ...member, typeParameters: resolveBounds( @@ -302,7 +341,7 @@ export const complete = ( self.modules, diagnostics, ), - fields: Object.freeze(fields), + fields: resolveFields(member.fields), }) }) const conformances = module.conformances.map((conformance) => { @@ -406,6 +445,9 @@ export const complete = ( enums: Object.freeze( closedMembers.filter((member): member is EnumFact => member._tag === 'EnumDeclaration'), ), + unions: Object.freeze( + closedMembers.filter((member): member is UnionFact => member._tag === 'UnionDeclaration'), + ), services: Object.freeze( closedMembers.filter( (member): member is ServiceFact => member._tag === 'ServiceDeclaration', @@ -500,6 +542,9 @@ export const complete = ( enums: Object.freeze( members.filter((member): member is EnumFact => member._tag === 'EnumDeclaration'), ), + unions: Object.freeze( + members.filter((member): member is UnionFact => member._tag === 'UnionDeclaration'), + ), services: Object.freeze( members.filter((member): member is ServiceFact => member._tag === 'ServiceDeclaration'), ), @@ -1184,7 +1229,11 @@ export const complete = ( continue } if (member._tag === 'RoleDeclaration' || member._tag === 'EnumDeclaration') continue - for (const field of member.fields) { + const fields = + member._tag === 'UnionDeclaration' + ? member.variants.flatMap((variant) => variant.fields) + : member.fields + for (const field of fields) { if ( field.declaredType._tag === 'Resolved' && containsPositionRestrictedBorrow(field.declaredType.type) @@ -1257,15 +1306,27 @@ export const complete = ( }) } if (member._tag === 'RoleDeclaration' || member._tag === 'EnumDeclaration') return member - const fields = member.fields.map((field) => - field.visibility === 'Public' - ? Object.freeze({ - ...field, - declaredType: attachExposure(field.declaredType, modules, diagnostics), - }) - : field, - ) - return Object.freeze({ ...member, fields: Object.freeze(fields) }) + const exposeFields = (fields: StructFact['fields']): StructFact['fields'] => + Object.freeze( + fields.map((field) => + field.visibility === 'Public' + ? Object.freeze({ + ...field, + declaredType: attachExposure(field.declaredType, modules, diagnostics), + }) + : field, + ), + ) + return member._tag === 'UnionDeclaration' + ? Object.freeze({ + ...member, + variants: Object.freeze( + member.variants.map((variant) => + Object.freeze({ ...variant, fields: exposeFields(variant.fields) }), + ), + ), + }) + : Object.freeze({ ...member, fields: exposeFields(member.fields) }) }) return Object.freeze({ ...module, @@ -1281,6 +1342,9 @@ export const complete = ( enums: Object.freeze( members.filter((member): member is EnumFact => member._tag === 'EnumDeclaration'), ), + unions: Object.freeze( + members.filter((member): member is UnionFact => member._tag === 'UnionDeclaration'), + ), services: Object.freeze( members.filter((member): member is ServiceFact => member._tag === 'ServiceDeclaration'), ), @@ -1373,6 +1437,9 @@ export const complete = ( enums: Object.freeze( members.filter((member): member is EnumFact => member._tag === 'EnumDeclaration'), ), + unions: Object.freeze( + members.filter((member): member is UnionFact => member._tag === 'UnionDeclaration'), + ), services: Object.freeze( members.filter((member): member is ServiceFact => member._tag === 'ServiceDeclaration'), ), diff --git a/packages/compiler/src/DeclarationFacts.ts b/packages/compiler/src/DeclarationFacts.ts index b5cab9126..5f69a368b 100644 --- a/packages/compiler/src/DeclarationFacts.ts +++ b/packages/compiler/src/DeclarationFacts.ts @@ -70,13 +70,61 @@ export interface ParameterId { readonly ordinal: number } -/** A deterministic field identity nested under its owning struct declaration. */ +/** A deterministic variant identity nested under its owning nominal union declaration. */ +export interface UnionVariantId { + readonly _tag: 'UnionVariantId' + readonly union: DeclarationId + readonly ordinal: number +} + +/** The aggregate declaration scope that owns one field identity. */ +export type FieldOwnerId = + | { readonly _tag: 'StructFieldOwnerId'; readonly declaration: DeclarationId } + | { readonly _tag: 'UnionVariantFieldOwnerId'; readonly variant: UnionVariantId } + +/** A deterministic field identity nested under one struct or nominal-union variant. */ export interface FieldId { readonly _tag: 'FieldId' - readonly struct: DeclarationId + readonly owner: FieldOwnerId readonly ordinal: number } +/** Returns the top-level declaration that transitively owns one aggregate field. */ +export const fieldDeclaration = (self: FieldId): DeclarationId => + self.owner._tag === 'StructFieldOwnerId' ? self.owner.declaration : self.owner.variant.union + +/** Tests canonical local field identity without relying on object reference equality. */ +export const sameFieldId = (left: FieldId, right: FieldId): boolean => { + if (left.ordinal !== right.ordinal || left.owner._tag !== right.owner._tag) return false + if (left.owner._tag === 'StructFieldOwnerId' && right.owner._tag === 'StructFieldOwnerId') { + return ( + left.owner.declaration.sourceId === right.owner.declaration.sourceId && + left.owner.declaration.ordinal === right.owner.declaration.ordinal + ) + } + if ( + left.owner._tag === 'UnionVariantFieldOwnerId' && + right.owner._tag === 'UnionVariantFieldOwnerId' + ) { + return ( + left.owner.variant.union.sourceId === right.owner.variant.union.sourceId && + left.owner.variant.union.ordinal === right.owner.variant.union.ordinal && + left.owner.variant.ordinal === right.owner.variant.ordinal + ) + } + return false +} + +/** Encodes one field identity as a stable key for maps, diagnostics, and backend selectors. */ +export const fieldIdKey = (self: FieldId): string => { + if (self.owner._tag === 'StructFieldOwnerId') { + const declaration = self.owner.declaration + return `struct:${declaration.sourceId}:${declaration.ordinal}:${self.ordinal}` + } + const variant = self.owner.variant + return `union:${variant.union.sourceId}:${variant.union.ordinal}:${variant.ordinal}:${self.ordinal}` +} + /** The canonical identity of one declaration: canonical module identity plus name. */ export interface CanonicalId { readonly _tag: 'CanonicalDeclarationId' @@ -522,9 +570,9 @@ export type FieldState = | { readonly _tag: 'Duplicate'; readonly original: FieldId; readonly cause: Diagnostic.Identity } | { readonly _tag: 'Unidentified' } -/** One ordered nominal struct field header. */ +/** One ordered aggregate field header shared by structs and nominal-union variants. */ export interface FieldFact { - readonly _tag: 'StructField' + readonly _tag: 'AggregateField' readonly id: FieldId readonly state: FieldState readonly visibility: 'Public' | 'Private' @@ -555,6 +603,51 @@ export interface StructFact { readonly syntax: SyntaxTree.Node } +/** The canonical identity of one uniquely named variant of a canonical nominal union. */ +export interface CanonicalUnionVariantId { + readonly _tag: 'CanonicalUnionVariantId' + readonly union: CanonicalId + readonly name: string +} + +export type UnionVariantCanonicalState = + | { readonly _tag: 'Canonical'; readonly id: CanonicalUnionVariantId } + | { + readonly _tag: 'Duplicate' + readonly original: CanonicalUnionVariantId + readonly cause: Diagnostic.Identity + } + | { readonly _tag: 'Unidentified' } + +/** One source-ordered unit or named-field variant subordinate to a nominal union. */ +export interface UnionVariantFact { + readonly _tag: 'UnionVariant' + readonly id: UnionVariantId + readonly canonical: UnionVariantCanonicalState + readonly name: DeclaredName + readonly kind: 'Unit' | 'Fields' + readonly fields: ReadonlyArray + readonly syntax: SyntaxTree.Node +} + +export type UnionValidity = + | { readonly _tag: 'Valid' } + | { readonly _tag: 'Invalid'; readonly causes: ReadonlyArray } + +/** One nominal tagged union declaration and its subordinate ordered variants. */ +export interface UnionFact { + readonly _tag: 'UnionDeclaration' + readonly id: DeclarationId + readonly canonical: CanonicalState + readonly visibility: 'Public' | 'Private' + readonly typeParameters: ReadonlyArray + readonly name: DeclaredName + readonly variants: ReadonlyArray + readonly dependency: StructDependency + readonly validity: UnionValidity + readonly syntax: SyntaxTree.Node +} + /** A deterministic member identity nested under its owning enum declaration. */ export interface EnumMemberId { readonly _tag: 'EnumMemberId' @@ -1338,6 +1431,7 @@ export type MemberFact = | DeclarationFact | StructFact | EnumFact + | UnionFact | ServiceFact | InterfaceFact | ConstantFact @@ -1397,6 +1491,24 @@ export type StructLookup = readonly declarations: ReadonlyArray } +export type UnionLookup = + | { readonly _tag: 'Resolved'; readonly spelling: string; readonly declaration: UnionFact } + | { readonly _tag: 'Missing'; readonly spelling: string } + | { + readonly _tag: 'Ambiguous' + readonly spelling: string + readonly declarations: ReadonlyArray + } + +export type UnionVariantLookup = + | { readonly _tag: 'Resolved'; readonly spelling: string; readonly variant: UnionVariantFact } + | { readonly _tag: 'Missing'; readonly spelling: string } + | { + readonly _tag: 'Ambiguous' + readonly spelling: string + readonly variants: ReadonlyArray + } + export type FieldLookup = | { readonly _tag: 'Resolved'; readonly spelling: string; readonly field: FieldFact } | { readonly _tag: 'Missing'; readonly spelling: string } @@ -1414,6 +1526,7 @@ export interface ModuleHeaders { readonly declarations: ReadonlyArray readonly structs: ReadonlyArray readonly enums: ReadonlyArray + readonly unions: ReadonlyArray readonly services: ReadonlyArray readonly interfaces: ReadonlyArray readonly constants: ReadonlyArray @@ -1556,6 +1669,31 @@ export const lookupStruct = (structs: ReadonlyArray, name: string): : Object.freeze({ _tag: 'Ambiguous', spelling: name, declarations: Object.freeze(matches) }) } +export const lookupUnion = (unions: ReadonlyArray, name: string): UnionLookup => { + const matches = unions.filter( + (union) => union.name._tag === 'Present' && union.name.spelling === name, + ) + const first = matches.at(0) + if (first === undefined) return Object.freeze({ _tag: 'Missing', spelling: name }) + return matches.length === 1 + ? Object.freeze({ _tag: 'Resolved', spelling: name, declaration: first }) + : Object.freeze({ _tag: 'Ambiguous', spelling: name, declarations: Object.freeze(matches) }) +} + +export const lookupUnionVariant = ( + variants: ReadonlyArray, + name: string, +): UnionVariantLookup => { + const matches = variants.filter( + (variant) => variant.name._tag === 'Present' && variant.name.spelling === name, + ) + const first = matches.at(0) + if (first === undefined) return Object.freeze({ _tag: 'Missing', spelling: name }) + return matches.length === 1 + ? Object.freeze({ _tag: 'Resolved', spelling: name, variant: first }) + : Object.freeze({ _tag: 'Ambiguous', spelling: name, variants: Object.freeze(matches) }) +} + export const lookupField = (fields: ReadonlyArray, name: string): FieldLookup => { const matches = fields.filter( (field) => field.name._tag === 'Present' && field.name.spelling === name, diff --git a/packages/compiler/src/Diagnostic.ts b/packages/compiler/src/Diagnostic.ts index 5208856de..09c739835 100644 --- a/packages/compiler/src/Diagnostic.ts +++ b/packages/compiler/src/Diagnostic.ts @@ -318,6 +318,12 @@ export const bodylessOpaqueResultCode = 'SEM0118' as const /** Stable code for effect-block return sites whose success types disagree. */ export const effectBlockReturnMismatchCode = 'SEM0163' as const +/** Stable code for a nominal union declaration with no variants. */ +export const emptyNominalUnionCode = 'SEM0164' as const +/** Stable code for a repeated variant name within one nominal union. */ +export const duplicateUnionVariantCode = 'SEM0165' as const +/** Stable code for a named-field variant whose braces contain no field. */ +export const emptyUnionVariantCode = 'SEM0166' as const /** Stable code for a use of a binding after its consuming move. */ export const useAfterMoveCode = 'OWN0001' as const @@ -517,6 +523,9 @@ export type Code = | typeof missingOpaqueRealizationCode | typeof bodylessOpaqueResultCode | typeof effectBlockReturnMismatchCode + | typeof emptyNominalUnionCode + | typeof duplicateUnionVariantCode + | typeof emptyUnionVariantCode | typeof useAfterMoveCode | typeof partialMoveCode | typeof explicitMoveRequiredCode @@ -574,6 +583,13 @@ export type Reason = } | { readonly _tag: 'ReservedTemplateSyntax' } | { readonly _tag: 'ReservedImportBinding'; readonly spelling: string } + | { readonly _tag: 'EmptyNominalUnion'; readonly union: string } + | { + readonly _tag: 'DuplicateUnionVariant' + readonly spelling: string + readonly originalSpan: SourceSpan.SourceSpan + } + | { readonly _tag: 'EmptyUnionVariant'; readonly variant: string } | { readonly _tag: 'UnknownModule'; readonly module: string } | { readonly _tag: 'SelfImport'; readonly module: string } | { readonly _tag: 'ReservedModuleIdentity'; readonly module: string } @@ -1658,6 +1674,49 @@ export const emptyEnum = (enumName: string, span: SourceSpan.SourceSpan): Diagno span, }) +/** Creates the diagnostic for a nominal union with no variants. */ +export const emptyNominalUnion = (unionName: string, span: SourceSpan.SourceSpan): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: emptyNominalUnionCode, + severity: 'error', + message: `Union ${unionName} must declare at least one variant`, + reason: Object.freeze({ _tag: 'EmptyNominalUnion', union: unionName }), + span, + }) + +/** Creates the diagnostic for a repeated variant name within one nominal union. */ +export const duplicateUnionVariant = ( + spelling: string, + originalSpan: SourceSpan.SourceSpan, + span: SourceSpan.SourceSpan, +): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: duplicateUnionVariantCode, + severity: 'error', + message: `Duplicate union variant ${spelling}`, + reason: Object.freeze({ _tag: 'DuplicateUnionVariant', spelling, originalSpan }), + span, + relatedSpans: Object.freeze([ + Object.freeze({ label: 'first declared here', span: originalSpan }), + ]), + }) + +/** Creates the diagnostic for braces used without any named variant field. */ +export const emptyUnionVariant = (variantName: string, span: SourceSpan.SourceSpan): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: emptyUnionVariantCode, + severity: 'error', + message: `Union variant ${variantName} must omit braces or declare at least one field`, + reason: Object.freeze({ _tag: 'EmptyUnionVariant', variant: variantName }), + span, + }) + export const unsupportedEnumRepresentation = ( spelling: string, allowed: ReadonlyArray, diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index 224e31318..5135fb113 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -1050,11 +1050,19 @@ export const intrinsicStruct = ( fields: Object.freeze( fieldTypes.map(([name, fieldType], fieldOrdinal) => Object.freeze({ - _tag: 'StructField' as const, - id: Object.freeze({ _tag: 'FieldId' as const, struct: id, ordinal: fieldOrdinal }), + _tag: 'AggregateField' as const, + id: Object.freeze({ + _tag: 'FieldId' as const, + owner: Object.freeze({ _tag: 'StructFieldOwnerId' as const, declaration: id }), + ordinal: fieldOrdinal, + }), state: Object.freeze({ _tag: 'Unique' as const, - id: Object.freeze({ _tag: 'FieldId' as const, struct: id, ordinal: fieldOrdinal }), + id: Object.freeze({ + _tag: 'FieldId' as const, + owner: Object.freeze({ _tag: 'StructFieldOwnerId' as const, declaration: id }), + ordinal: fieldOrdinal, + }), }), visibility: 'Public' as const, name: Object.freeze({ _tag: 'Present' as const, spelling: name, token }), diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index 91fd283c5..8df31bf05 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -130,6 +130,8 @@ const memberSignature = (member: DeclarationFacts.MemberFact): string => { : `<${member.typeParameters.map((parameter) => typeText(parameter.type)).join(', ')}>` if (member._tag === 'StructDeclaration') return `struct${parameters} · ${member.fields.length} field${member.fields.length === 1 ? '' : 's'}` + if (member._tag === 'UnionDeclaration') + return `union${parameters} · ${member.variants.length} variant${member.variants.length === 1 ? '' : 's'}` if (member._tag === 'EnumDeclaration') { const representation = member.representation._tag === 'Available' diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index dfb90babe..4da4c2267 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -1,6 +1,6 @@ import * as CleanupPlan from './CleanupPlan.js' import * as ConformanceProof from './ConformanceProof.js' -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import type * as DeclarationIndex from './DeclarationIndex.js' import * as Diagnostic from './Diagnostic.js' import * as ExecutionPackage from './ExecutionPackage.js' @@ -1061,7 +1061,10 @@ export const catalog = ( _tag: 'LayoutField' as const, id: Object.freeze({ _tag: 'FieldId' as const, - struct: structId, + owner: Object.freeze({ + _tag: 'StructFieldOwnerId' as const, + declaration: structId, + }), ordinal: fieldOrdinal, }), name, @@ -3882,11 +3885,7 @@ const fieldSlice = ( if (node._tag !== 'ProductShape') return undefined let fieldOffset = offset for (const candidate of node.fields) { - if ( - candidate.field.ordinal === field.ordinal && - candidate.field.struct.sourceId === field.struct.sourceId && - candidate.field.struct.ordinal === field.struct.ordinal - ) { + if (DeclarationFacts.sameFieldId(candidate.field, field)) { return fieldSlice(candidate.shape, rest, fieldOffset) } fieldOffset += candidate.shape.laneCount diff --git a/packages/compiler/src/LayoutEncode.ts b/packages/compiler/src/LayoutEncode.ts index 26a22e01f..df70f390e 100644 --- a/packages/compiler/src/LayoutEncode.ts +++ b/packages/compiler/src/LayoutEncode.ts @@ -1,3 +1,4 @@ +import * as DeclarationFacts from './DeclarationFacts.js' import * as ExecutionPackage from './ExecutionPackage.js' import * as Hir from './Hir.js' import type { @@ -183,7 +184,7 @@ export const encode = (self: Plan): string => case 'StringByteLengthSelector': return 'byte-length' case 'FieldId': - return `${selector.struct.sourceId}#${selector.struct.ordinal}.${selector.ordinal}` + return DeclarationFacts.fieldIdKey(selector) default: return '' } diff --git a/packages/compiler/src/LayoutVerify.ts b/packages/compiler/src/LayoutVerify.ts index 52f00ab5f..d7c62eca5 100644 --- a/packages/compiler/src/LayoutVerify.ts +++ b/packages/compiler/src/LayoutVerify.ts @@ -1,4 +1,4 @@ -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import * as Diagnostic from './Diagnostic.js' import * as FieldRealization from './FieldRealization.js' import { alignUp } from './internal/Align.js' @@ -839,9 +839,7 @@ const commonViolations = ( } const fieldIdEquals = (left: DeclarationFacts.FieldId, right: DeclarationFacts.FieldId): boolean => - left.ordinal === right.ordinal && - left.struct.sourceId === right.struct.sourceId && - left.struct.ordinal === right.struct.ordinal + DeclarationFacts.sameFieldId(left, right) /** Compares two compiler-planned physical selectors. */ export const selectorEquals = (left: Selector, right: Selector): boolean => { diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 89f92b547..04c5205e1 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -12,6 +12,7 @@ import { } from './CleanupEmission.js' import * as CleanupPlan from './CleanupPlan.js' import * as ConformanceProof from './ConformanceProof.js' +import * as DeclarationFacts from './DeclarationFacts.js' import type { LoweredExpression } from './EffectLowering.js' import { borrowedWriteRoot, @@ -1561,11 +1562,8 @@ export function lowerExpressionInner( const value = loweredFields.get(field.field.ordinal) const declared = representation?._tag === 'Aggregate' - ? representation.fields.find( - (candidate) => - candidate.id.ordinal === field.field.ordinal && - candidate.id.struct.sourceId === field.field.struct.sourceId && - candidate.id.struct.ordinal === field.field.struct.ordinal, + ? representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), ) : undefined const stored = diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index a9eef5149..39ab98e94 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -1,5 +1,5 @@ import type * as CleanupPlan from './CleanupPlan.js' -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import * as ExecutionPackage from './ExecutionPackage.js' import * as ExecutionTransition from './ExecutionTransition.js' import * as FieldRealization from './FieldRealization.js' @@ -881,11 +881,8 @@ const placeType = ( : undefined const field = entry?.representation._tag === 'Aggregate' - ? entry.representation.fields.find( - (candidate) => - candidate.id.ordinal === selector.field.ordinal && - candidate.id.struct.sourceId === selector.field.struct.sourceId && - candidate.id.struct.ordinal === selector.field.struct.ordinal, + ? entry.representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, selector.field), ) : undefined current = field?.type @@ -930,11 +927,8 @@ const fieldPathType = ( : undefined const field: Layout.Field | undefined = entry?.representation._tag === 'Aggregate' - ? entry.representation.fields.find( - (candidate) => - candidate.id.ordinal === selector.ordinal && - candidate.id.struct.sourceId === selector.struct.sourceId && - candidate.id.struct.ordinal === selector.struct.ordinal, + ? entry.representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, selector), ) : undefined current = field?.type @@ -1361,9 +1355,7 @@ const cleanupMatchesSemanticType = ( const expected = representation.fields.at(ordinal) return ( expected !== undefined && - field.field.ordinal === expected.id.ordinal && - field.field.struct.sourceId === expected.id.struct.sourceId && - field.field.struct.ordinal === expected.id.struct.ordinal && + DeclarationFacts.sameFieldId(field.field, expected.id) && cleanupMatchesSemanticType(layout, field.cleanup, expected.type, next) ) }) diff --git a/packages/compiler/src/ModuleSurface.ts b/packages/compiler/src/ModuleSurface.ts index a9e316fb9..dd071c567 100644 --- a/packages/compiler/src/ModuleSurface.ts +++ b/packages/compiler/src/ModuleSurface.ts @@ -1858,6 +1858,25 @@ const struct = (value: DeclarationFacts.StructFact): string => structDependency(value.dependency), ]) +const unionVariant = (value: DeclarationFacts.UnionVariantFact): string => + record('UnionVariant', [ + number(value.id.ordinal), + value.canonical._tag, + name(value.name), + array(value.fields.map(field)), + ]) + +const unionDeclaration = (value: DeclarationFacts.UnionFact): string => + record('UnionDeclaration', [ + declarationIdOrdinal(value.id), + canonicalState(value.canonical), + value.visibility, + array(value.typeParameters.map(typeParameter)), + name(value.name), + array(value.variants.map(unionVariant)), + value.validity._tag, + ]) + const serviceOperationState = (value: DeclarationFacts.ServiceOperationState): string => { switch (value._tag) { case 'Unique': @@ -1945,6 +1964,8 @@ const member = (value: DeclarationFacts.MemberFact): string => { return declaration(value) case 'StructDeclaration': return struct(value) + case 'UnionDeclaration': + return unionDeclaration(value) case 'EnumDeclaration': return enumDeclaration(value) case 'ServiceDeclaration': diff --git a/packages/compiler/src/NameResolution.ts b/packages/compiler/src/NameResolution.ts index 7db340405..374b7c5a9 100644 --- a/packages/compiler/src/NameResolution.ts +++ b/packages/compiler/src/NameResolution.ts @@ -385,6 +385,7 @@ export const scopedModule = (declaration: DeclarationFacts.MemberFact): string | if ( declaration._tag !== 'StructDeclaration' && declaration._tag !== 'EnumDeclaration' && + declaration._tag !== 'UnionDeclaration' && declaration._tag !== 'ServiceDeclaration' && declaration._tag !== 'InterfaceDeclaration' ) @@ -538,6 +539,7 @@ const resolvedType = ( const nominalOf = (declaration: DeclarationFacts.MemberFact): Type.Nominal | undefined => (declaration._tag === 'StructDeclaration' || declaration._tag === 'EnumDeclaration' || + declaration._tag === 'UnionDeclaration' || declaration._tag === 'ServiceDeclaration' || declaration._tag === 'InterfaceDeclaration') && declaration.canonical._tag === 'Canonical' diff --git a/packages/compiler/src/NativeAggregate.ts b/packages/compiler/src/NativeAggregate.ts index 39a5f2bb5..d55548f18 100644 --- a/packages/compiler/src/NativeAggregate.ts +++ b/packages/compiler/src/NativeAggregate.ts @@ -9,7 +9,7 @@ import type * as LlvmType from '@silklang/llvm/Type' import type * as Value from '@silklang/llvm/Value' import * as Effect from 'effect/Effect' import * as CleanupPlan from './CleanupPlan.js' -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import * as Layout from './Layout.js' import * as LayoutVerify from './LayoutVerify.js' import * as LocalSharedControlBlock from './LocalSharedControlBlock.js' @@ -450,9 +450,7 @@ export const dropThroughPlan = Effect.fnUntraced(function* ( return first !== undefined && first._tag === 'FieldId' && value !== undefined && - first.ordinal === field.field.ordinal && - first.struct.ordinal === field.field.struct.ordinal && - first.struct.sourceId === field.field.struct.sourceId + DeclarationFacts.sameFieldId(first, field.field) ? [value] : [] }) diff --git a/packages/compiler/src/NativePlaceOperation.ts b/packages/compiler/src/NativePlaceOperation.ts index 895a81405..47695f8db 100644 --- a/packages/compiler/src/NativePlaceOperation.ts +++ b/packages/compiler/src/NativePlaceOperation.ts @@ -3,6 +3,7 @@ import * as Constant from '@silklang/llvm/Constant' import * as FunctionBody from '@silklang/llvm/FunctionBody' import * as Value from '@silklang/llvm/Value' import * as Effect from 'effect/Effect' +import * as DeclarationFacts from './DeclarationFacts.js' import * as Layout from './Layout.js' import * as LayoutVerify from './LayoutVerify.js' import * as Mir from './Mir.js' @@ -184,11 +185,8 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op if (selector._tag === 'FieldSelector') { if (selectedLayout?.representation._tag !== 'Aggregate') throw new RangeError('LLVM borrow field lost its aggregate layout') - const field = selectedLayout.representation.fields.find( - (candidate) => - candidate.id.ordinal === selector.field.ordinal && - candidate.id.struct.sourceId === selector.field.struct.sourceId && - candidate.id.struct.ordinal === selector.field.struct.ordinal, + const field = selectedLayout.representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, selector.field), ) if (field === undefined) throw new RangeError('LLVM borrow field lost its field layout') staticOffset += field.offset @@ -446,9 +444,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op return first !== undefined && first._tag === 'FieldId' && selected !== undefined && - first.ordinal === operation.field.ordinal && - first.struct.sourceId === operation.field.struct.sourceId && - first.struct.ordinal === operation.field.struct.ordinal + DeclarationFacts.sameFieldId(first, operation.field) ? [selected] : [] }) @@ -657,9 +653,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op if (selector._tag === 'FieldSelector') { if ( physical._tag !== 'FieldId' || - physical.ordinal !== selector.field.ordinal || - physical.struct.sourceId !== selector.field.struct.sourceId || - physical.struct.ordinal !== selector.field.struct.ordinal + !DeclarationFacts.sameFieldId(physical, selector.field) ) { return [] } @@ -959,9 +953,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op if (selector._tag === 'FieldSelector') { if ( physical._tag !== 'FieldId' || - physical.ordinal !== selector.field.ordinal || - physical.struct.sourceId !== selector.field.struct.sourceId || - physical.struct.ordinal !== selector.field.struct.ordinal + !DeclarationFacts.sameFieldId(physical, selector.field) ) { matches = false break diff --git a/packages/compiler/src/Parser/Expression.ts b/packages/compiler/src/Parser/Expression.ts index 5f1c5affb..8829546ca 100644 --- a/packages/compiler/src/Parser/Expression.ts +++ b/packages/compiler/src/Parser/Expression.ts @@ -132,12 +132,6 @@ export const hasAppliedUnionVariant = (state: State): boolean => { return false } -export const hasBareUnionVariantFields = (state: State): boolean => - nextSignificantKind(state) === 'Identifier' && - peek(state, 1) === 'Dot' && - peek(state, 2) === 'Identifier' && - peek(state, 3) === 'LeftBrace' - export const parseIntegerLiteralExpression = (initial: State): NodeResult => { if (nextSignificantKind(initial) === 'Minus') { const minus = expect(initial, 'Minus', ['DecimalInteger', ...expressionFollowing]) @@ -292,7 +286,7 @@ export const primaryKind = ( if (member === 'LeftParenthesis') return 'Call' const afterMember = peek(state, 3) if (afterMember === 'LeftParenthesis') return 'Call' - if (afterMember === 'LeftBrace') return allowStructLiteral ? 'UnionVariant' : 'Identifier' + if (afterMember === 'LeftBrace') return allowStructLiteral ? 'StructLiteral' : 'Identifier' } return 'Identifier' } @@ -743,7 +737,6 @@ export const isRowWithoutStart = (state: State): boolean => export const isNominalPatternStart = (state: State): boolean => { if (nextSignificantKind(state) !== 'Identifier') return false if (hasAppliedUnionVariant(state)) return true - if (hasBareUnionVariantFields(state)) return true if (hasCompleteAppliedPostfix(state, 'LeftBrace')) return true const following = peek(state, 1) if (following === 'LeftBrace') return true @@ -843,8 +836,7 @@ export function parsePattern( }) } if (isEnumMemberPatternStart(initial)) return parseEnumMemberPattern(initial) - if (hasAppliedUnionVariant(initial) || hasBareUnionVariantFields(initial)) - return parseUnionVariantPattern(initial) + if (hasAppliedUnionVariant(initial)) return parseUnionVariantPattern(initial) const kind = nextSignificantKind(initial) if (kind === 'DecimalInteger' || (kind === 'Minus' && peek(initial, 1) === 'DecimalInteger')) return parseIntegerPattern(initial) @@ -914,10 +906,9 @@ export function parseUnionVariantPattern(initial: State): NodeResult { ]) state = colon.state if (isNominalPatternStart(state)) { - const nested = - hasAppliedUnionVariant(state) || hasBareUnionVariantFields(state) - ? parseUnionVariantPattern(state) - : parseNominalPattern(state) + const nested = hasAppliedUnionVariant(state) + ? parseUnionVariantPattern(state) + : parseNominalPattern(state) fieldChildren = Object.freeze([...fieldChildren, ...colon.elements, nested.node]) state = nested.state } else { diff --git a/packages/compiler/src/Presentation.ts b/packages/compiler/src/Presentation.ts index 792ae82f9..4a462b8ab 100644 --- a/packages/compiler/src/Presentation.ts +++ b/packages/compiler/src/Presentation.ts @@ -19,6 +19,7 @@ export type Presentation = readonly functionKind: DeclarationFacts.DeclarationFact['functionKind'] }) | (Base & { readonly _tag: 'StructPresentation'; readonly name: string }) + | (Base & { readonly _tag: 'UnionPresentation'; readonly name: string }) | (Base & { readonly _tag: 'EnumPresentation'; readonly name: string }) | (Base & { readonly _tag: 'EnumMemberPresentation'; readonly name: string }) | (Base & { readonly _tag: 'EnumOperationPresentation'; readonly name: string }) @@ -181,6 +182,21 @@ export const structDeclaration = (self: DeclarationFacts.StructFact): Presentati }) } +/** Renders a nominal tagged-union declaration without expanding its variants. */ +export const unionDeclaration = (self: DeclarationFacts.UnionFact): Presentation => { + const name = self.name._tag === 'Present' ? self.name.spelling : '_' + const visibility = self.visibility === 'Public' ? 'pub ' : '' + const typeParameters = + self.typeParameters.length === 0 + ? '' + : `<${self.typeParameters.map(typeParameterName).join(', ')}>` + return Object.freeze({ + _tag: 'UnionPresentation', + name, + text: `${visibility}union ${name}${typeParameters}`, + }) +} + /** Renders one nominal service contract without expanding its operation list. */ export const serviceDeclaration = ( self: DeclarationFacts.ServiceFact | DeclarationFacts.InterfaceFact, diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index 84d6d0dd6..a4d417420 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -151,15 +151,19 @@ const locationOfField = ( index: DeclarationIndex.Index, field: DeclarationFacts.FieldFact, ): DeclarationLocation | undefined => { + const declarationId = DeclarationFacts.fieldDeclaration(field.id) + const module = index.modules.find((candidate) => candidate.module === declarationId.sourceId) + const owner = field.id.owner const current = - index.modules - .find((module) => module.module === field.id.struct.sourceId) - ?.structs.find( - (struct) => - struct.id.sourceId === field.id.struct.sourceId && - struct.id.ordinal === field.id.struct.ordinal, - ) - ?.fields.find((candidate) => candidate.id.ordinal === field.id.ordinal) ?? field + (owner._tag === 'StructFieldOwnerId' + ? module?.structs + .find((struct) => struct.id.ordinal === declarationId.ordinal) + ?.fields.find((candidate) => DeclarationFacts.sameFieldId(candidate.id, field.id)) + : module?.unions + .find((union) => union.id.ordinal === declarationId.ordinal) + ?.variants.find((variant) => variant.id.ordinal === owner.variant.ordinal) + ?.fields.find((candidate) => DeclarationFacts.sameFieldId(candidate.id, field.id))) ?? + field return current.name._tag === 'Present' ? location(current.name.token.span.sourceId, current.syntax.span, current.name.token.span) : undefined @@ -1200,7 +1204,11 @@ const collectMember = ( return } if (member._tag === 'RoleDeclaration') return - for (const field of member.fields) { + const fields = + member._tag === 'UnionDeclaration' + ? member.variants.flatMap((variant) => variant.fields) + : member.fields + for (const field of fields) { if (field.name._tag === 'Present') push( pending, @@ -1436,7 +1444,7 @@ export const identityKey = (identity: Identity): string => { case 'PatternBindingIdentity': return `pattern:${JSON.stringify(identity.id)}` case 'FieldIdentity': - return `field:${identity.id.struct.sourceId}:${identity.id.struct.ordinal}:${identity.id.ordinal}` + return `field:${DeclarationFacts.fieldIdKey(identity.id)}` case 'EnumMemberIdentity': return `enum-member:${identity.id.enum.module}.${identity.id.enum.name}.${identity.id.name}` case 'EnumAssociatedOperationIdentity': diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index ca344f2cb..3ebafd16f 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '314c09c33a1b2a2bb9c213a907a390980b69f4ac3a10c235ffb91413f2b99ca9' +export const compilerDigest = 'cff32c3cd19cd28ff1e92d72c372f0a081f5fb0f0fb0ae1a0456066a36247cca' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 48069b502..c058fbdaa 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -16,7 +16,7 @@ import * as Backend from './Backend.js' import { symbolFor } from './Backend.js' import * as CleanupPlan from './CleanupPlan.js' import * as CoroutineFrame from './CoroutineFrame.js' -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import * as ExecutionPackage from './ExecutionPackage.js' import * as ExecutionTransition from './ExecutionTransition.js' import * as FloatingPoint from './FloatingPoint.js' @@ -1769,11 +1769,8 @@ const makeOperationContext = ( children: Object.freeze( plan_.fields.flatMap((field) => { if (!CleanupPlan.hasHook(field.cleanup)) return [] - const layoutField = representation.fields.find( - (candidate) => - candidate.id.ordinal === field.field.ordinal && - candidate.id.struct.ordinal === field.field.struct.ordinal && - candidate.id.struct.sourceId === field.field.struct.sourceId, + const layoutField = representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), ) return layoutField === undefined ? [] @@ -2219,9 +2216,7 @@ const makeOperationContext = ( return first !== undefined && first._tag === 'FieldId' && value !== undefined && - first.ordinal === field.field.ordinal && - first.struct.ordinal === field.field.struct.ordinal && - first.struct.sourceId === field.field.struct.sourceId + DeclarationFacts.sameFieldId(first, field.field) ? [value] : [] }), @@ -2398,11 +2393,8 @@ const makeOperationContext = ( return Object.freeze({ children: Object.freeze( plan_.fields.flatMap((field) => { - const layoutField = representation.fields.find( - (candidate) => - candidate.id.ordinal === field.field.ordinal && - candidate.id.struct.ordinal === field.field.struct.ordinal && - candidate.id.struct.sourceId === field.field.struct.sourceId, + const layoutField = representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), ) return layoutField === undefined ? [] @@ -4817,11 +4809,8 @@ const emitBeginLoanOperation = ( if (selector._tag === 'FieldSelector') { if (selectedLayout?.representation._tag !== 'Aggregate') throw new RangeError('Wasm borrow field selector lost its aggregate layout') - const field = selectedLayout.representation.fields.find( - (candidate) => - candidate.id.ordinal === selector.field.ordinal && - candidate.id.struct.sourceId === selector.field.struct.sourceId && - candidate.id.struct.ordinal === selector.field.struct.ordinal, + const field = selectedLayout.representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, selector.field), ) if (field === undefined) throw new RangeError('Wasm borrow field selector lost its field layout') @@ -5008,9 +4997,7 @@ const emitProjectOperation = ( return field !== undefined && field._tag === 'FieldId' && source !== undefined && - field.ordinal === operation.field.ordinal && - field.struct.sourceId === operation.field.struct.sourceId && - field.struct.ordinal === operation.field.struct.ordinal + DeclarationFacts.sameFieldId(field, operation.field) ? [source] : [] }) @@ -5125,9 +5112,7 @@ const emitReadPlaceOperation = ( if (selector._tag === 'FieldSelector') { if ( physical._tag !== 'FieldId' || - physical.ordinal !== selector.field.ordinal || - physical.struct.sourceId !== selector.field.struct.sourceId || - physical.struct.ordinal !== selector.field.struct.ordinal + !DeclarationFacts.sameFieldId(physical, selector.field) ) { return [] } @@ -5310,9 +5295,7 @@ const emitWritePlaceOperation = ( if (selector._tag === 'FieldSelector') { if ( physical._tag !== 'FieldId' || - physical.ordinal !== selector.field.ordinal || - physical.struct.sourceId !== selector.field.struct.sourceId || - physical.struct.ordinal !== selector.field.struct.ordinal + !DeclarationFacts.sameFieldId(physical, selector.field) ) { matches = false break diff --git a/packages/compiler/test/DeclarationIndex.test.ts b/packages/compiler/test/DeclarationIndex.test.ts index 6ab9178ae..b67ce66d4 100644 --- a/packages/compiler/test/DeclarationIndex.test.ts +++ b/packages/compiler/test/DeclarationIndex.test.ts @@ -949,6 +949,83 @@ it.effect('indexes mixed struct and function declarations in one canonical names }), ) +it.effect('indexes generic nominal unions with parent-scoped variants and fields', () => + Effect.gen(function* () { + const index = yield* collect('root', [ + [ + 'root', + `pub union Result { Success { pub value: A, next: Other }, Failure { error: E }, Pending } +union Other { Success { value: bool } }`, + ], + ]) + const module = index.modules.at(0) + const result = module?.unions.at(0) + const other = module?.unions.at(1) + + assert.deepEqual( + module?.members.map((member) => member._tag), + ['UnionDeclaration', 'UnionDeclaration'], + ) + assert.deepEqual( + result?.typeParameters.map((parameter) => parameter.type.name), + ['A', 'E'], + ) + assert.deepEqual( + result?.variants.map((variant) => [ + variant.name._tag === 'Present' ? variant.name.spelling : '_', + variant.kind, + ]), + [ + ['Success', 'Fields'], + ['Failure', 'Fields'], + ['Pending', 'Unit'], + ], + ) + assert.strictEqual(result?.validity._tag, 'Valid') + assert.strictEqual(result?.variants.at(0)?.fields.at(0)?.visibility, 'Public') + assert.strictEqual(result?.variants.at(0)?.fields.at(1)?.declaredType._tag, 'Resolved') + assert.notDeepEqual(result?.variants.at(0)?.canonical, other?.variants.at(0)?.canonical) + assert.notDeepEqual( + result?.variants.at(0)?.fields.at(0)?.id, + other?.variants.at(0)?.fields.at(0)?.id, + ) + assert.deepEqual(index.diagnostics, []) + }), +) + +it.effect('diagnoses invalid nominal unions while preserving valid siblings', () => + Effect.gen(function* () { + const source = `union Empty {} +union Damaged { Same, Same, EmptyFields {}, Good { value: Missing }, Tail } +struct Damaged {}` + const index = yield* collect('root', [['root', source]]) + const unions = index.modules.at(0)?.unions ?? [] + const damaged = unions.at(1) + + assert.deepEqual( + damaged?.variants.map((variant) => + variant.name._tag === 'Present' ? variant.name.spelling : '_', + ), + ['Same', 'Same', 'EmptyFields', 'Good', 'Tail'], + ) + assert.strictEqual(damaged?.variants.at(4)?.canonical._tag, 'Canonical') + assert.strictEqual(damaged?.validity._tag, 'Invalid') + assert.deepEqual( + index.diagnostics.map((diagnostic) => diagnostic.code), + ['SEM0164', 'SEM0165', 'SEM0166', 'SEM0001', 'SEM0003'], + ) + const duplicate = index.diagnostics.find((diagnostic) => diagnostic.code === 'SEM0165') + assert.deepEqual( + duplicate?.relatedSpans.map((related) => source.slice(related.span.start, related.span.end)), + ['Same'], + ) + assert.strictEqual( + duplicate === undefined ? undefined : source.slice(duplicate.span.start, duplicate.span.end), + 'Same', + ) + }), +) + it.effect('retains duplicate and damaged struct fields without losing later fields', () => Effect.gen(function* () { const index = yield* collect('root', [ From 525821843568f4c1b5f7a38f2dad007938903d72 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 13:15:04 -0300 Subject: [PATCH 04/42] feat(compiler): publish nominal union surfaces --- apps/docs/content/language/diagnostics.md | 2 +- openspec/changes/add-nominal-unions/tasks.md | 4 +- packages/compiler/src/Analysis.ts | 83 ++++++++++++++++--- packages/compiler/src/Completion.ts | 33 ++++++++ .../compiler/src/DeclarationCompletion.ts | 46 +++++++--- packages/compiler/src/DeclarationFacts.ts | 19 +++++ .../compiler/src/DeclarationResolution.ts | 46 ++++++---- packages/compiler/src/Diagnostic.ts | 19 +++-- packages/compiler/src/DocBlock.ts | 3 + packages/compiler/src/ModuleSurface.ts | 17 +++- packages/compiler/src/Presentation.ts | 29 +++++++ packages/compiler/src/SemanticOccurrence.ts | 42 +++++++++- .../src/ToolchainIntegrity.generated.ts | 2 +- .../compiler/test/DeclarationIndex.test.ts | 23 +++++ .../compiler/test/EditorIntelligence.test.ts | 71 ++++++++++++++++ packages/compiler/test/ModuleSurface.test.ts | 32 +++++++ 16 files changed, 410 insertions(+), 61 deletions(-) diff --git a/apps/docs/content/language/diagnostics.md b/apps/docs/content/language/diagnostics.md index 3168740b3..8e66799bc 100644 --- a/apps/docs/content/language/diagnostics.md +++ b/apps/docs/content/language/diagnostics.md @@ -73,7 +73,7 @@ There are 189 codes in total. | `SEM0017` | | `Duplicate field name ` | | `SEM0018` | | `Expected a type, found ` | | `SEM0019` | | `Public declaration exposes private type ` | -| `SEM0020` | | `Inline recursive struct layout: ` | +| `SEM0020` | | `Inline recursive aggregate layout: ` | | `SEM0021` | | `Cannot construct because its raw constructor is not available at this site` | | `SEM0022` | | ` has no field ` | | `SEM0023` | | `Field is initialized more than once` | diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 83714093d..5080eaff5 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -11,8 +11,8 @@ - [x] 2.1 Add canonical `UnionFact`, subordinate variant identities, and variant-scoped field ownership while generalizing shared field facts away from struct-only owners, and verify identity tests distinguish same-spelled variants and fields under different parents. - [x] 2.2 Collect unions in the ordinary cross-kind module namespace with parent parameters and source-ordered variants before bodies, and verify forward declarations, duplicates, empty unions, and cross-kind collisions in declaration-index tests. -- [ ] 2.3 Resolve every variant field type, visibility exposure, generic reference, and inline aggregate dependency before body analysis, and verify invalid fields preserve sibling facts while making the complete parent non-executable. -- [ ] 2.4 Encode union declarations in deterministic module semantic surfaces, and verify encode/decode, equality, and dependency invalidation respond to variant order, kind, field, type, visibility, bound, and availability changes but ignore body-only edits. +- [x] 2.3 Resolve every variant field type, visibility exposure, generic reference, and inline aggregate dependency before body analysis, and verify invalid fields preserve sibling facts while making the complete parent non-executable. +- [x] 2.4 Encode union declarations in deterministic module semantic surfaces, and verify encode/decode, equality, and dependency invalidation respond to variant order, kind, field, type, visibility, bound, and availability changes but ignore body-only edits. - [ ] 2.5 Extend semantic occurrence, navigation, completion, documentation, and Analysis facade queries for parent, variant, and field identities, and verify go-to-definition/reference tests use canonical facts rather than syntax reconstruction. ## 3. Type Application and Variant Construction diff --git a/packages/compiler/src/Analysis.ts b/packages/compiler/src/Analysis.ts index 14a2de5dc..4a73714c7 100644 --- a/packages/compiler/src/Analysis.ts +++ b/packages/compiler/src/Analysis.ts @@ -336,6 +336,19 @@ const serviceOperationForIdentity = ( operation.name._tag === 'Present' && operation.name.spelling === identity.id.name, ) +const unionVariantForIdentity = ( + self: FrontendSnapshot, + identity: Extract, +): readonly [DeclarationFacts.UnionFact, DeclarationFacts.UnionVariantFact] | undefined => { + const union = DeclarationFacts.byCanonical(self.index, identity.id.union) + if (union?._tag !== 'UnionDeclaration') return undefined + const variant = union.variants.find( + (candidate) => + candidate.canonical._tag === 'Canonical' && candidate.canonical.id.name === identity.id.name, + ) + return variant === undefined ? undefined : Object.freeze([union, variant] as const) +} + const syntaxForIdentity = ( self: FrontendSnapshot, identity: SemanticOccurrence.Identity, @@ -356,6 +369,8 @@ const syntaxForIdentity = ( (member) => member.canonical._tag === 'Canonical' && member.canonical.id.name === identity.id.name, )?.syntax + if (identity._tag === 'UnionVariantIdentity') + return unionVariantForIdentity(self, identity)?.[1].syntax if (identity._tag === 'EnumAssociatedOperationIdentity') return DeclarationFacts.byCanonical(self.index, identity.id.enum)?.syntax if (identity._tag === 'TypeParameterIdentity') { @@ -392,13 +407,17 @@ const syntaxForIdentity = ( return undefined } if (identity._tag === 'FieldIdentity') { - for (const headers of self.index.modules) - for (const declaration of headers.structs) { - const field = declaration.fields.find((candidate) => - DeclarationFacts.sameFieldId(candidate.id, identity.id), - ) - if (field !== undefined) return field.syntax + for (const headers of self.index.modules) { + const fields = [ + ...headers.structs.flatMap((declaration) => declaration.fields), + ...headers.unions.flatMap((declaration) => + declaration.variants.flatMap((variant) => variant.fields), + ), + ] + for (const field of fields) { + if (DeclarationFacts.sameFieldId(field.id, identity.id)) return field.syntax } + } } return undefined } @@ -442,6 +461,7 @@ const hoverPresentation = ( const nominalDeclarationType = ( declaration: | DeclarationFacts.StructFact + | DeclarationFacts.UnionFact | DeclarationFacts.EnumFact | DeclarationFacts.ContractFact, ): Type.Nominal | undefined => @@ -479,6 +499,11 @@ const presentationOfIdentity = ( Presentation.enumDeclaration(declaration), nominalDeclarationType(declaration), ) + if (declaration?._tag === 'UnionDeclaration') + return hoverPresentation( + Presentation.unionDeclaration(declaration), + nominalDeclarationType(declaration), + ) if (declaration?._tag === 'ServiceDeclaration') return hoverPresentation( Presentation.serviceDeclaration(declaration), @@ -535,14 +560,17 @@ const presentationOfIdentity = ( return undefined } if (identity._tag === 'FieldIdentity') { - for (const headers of self.index.modules) - for (const declaration of headers.structs) { - const field = declaration.fields.find((candidate) => - DeclarationFacts.sameFieldId(candidate.id, identity.id), - ) - if (field !== undefined) + for (const headers of self.index.modules) { + const fields = [ + ...headers.structs.flatMap((declaration) => declaration.fields), + ...headers.unions.flatMap((declaration) => + declaration.variants.flatMap((variant) => variant.fields), + ), + ] + for (const field of fields) + if (DeclarationFacts.sameFieldId(field.id, identity.id)) return hoverPresentation(Presentation.field(field), declaredType(field.declaredType)) - } + } return undefined } if (identity._tag === 'BindingIdentity') { @@ -641,6 +669,15 @@ const presentationOfIdentity = ( ? undefined : hoverPresentation(Presentation.enumMember(enum_, member), nominalDeclarationType(enum_)) } + if (identity._tag === 'UnionVariantIdentity') { + const selected = unionVariantForIdentity(self, identity) + return selected === undefined + ? undefined + : hoverPresentation( + Presentation.unionVariant(selected[0], selected[1]), + nominalDeclarationType(selected[0]), + ) + } if (identity._tag === 'EnumAssociatedOperationIdentity') { const enum_ = DeclarationFacts.byCanonical(self.index, identity.id.enum) const operation = @@ -980,6 +1017,26 @@ export const structByName = ( spelling: string, ): DeclarationFacts.StructLookup => DeclarationFacts.struct(self.index, module, spelling) +/** Looks up one nominal tagged-union declaration. */ +export const unionByName = ( + self: FrontendSnapshot, + module: string, + spelling: string, +): DeclarationFacts.UnionLookup => DeclarationFacts.unionByName(self.index, module, spelling) + +/** Looks up one declaration-ordered variant from a resolved nominal union. */ +export const unionVariantByName = ( + declaration: DeclarationFacts.UnionFact, + spelling: string, +): DeclarationFacts.UnionVariantLookup => + DeclarationFacts.lookupUnionVariant(declaration.variants, spelling) + +/** Looks up one declaration-ordered field within a resolved nominal-union variant. */ +export const unionVariantFieldByName = ( + variant: DeclarationFacts.UnionVariantFact, + spelling: string, +): DeclarationFacts.FieldLookup => DeclarationFacts.lookupField(variant.fields, spelling) + /** Looks up one declaration-ordered field from a resolved nominal struct. */ export const fieldByName = ( declaration: DeclarationFacts.StructFact, diff --git a/packages/compiler/src/Completion.ts b/packages/compiler/src/Completion.ts index d580a63d0..87cca5fac 100644 --- a/packages/compiler/src/Completion.ts +++ b/packages/compiler/src/Completion.ts @@ -284,6 +284,26 @@ const enumCandidates = (enum_: DeclarationFacts.EnumFact): ReadonlyArray => + Object.freeze( + union.variants.flatMap( + (variant): ReadonlyArray => + variant.name._tag !== 'Present' || variant.canonical._tag !== 'Canonical' + ? [] + : [ + candidate({ + identity: semantic( + Object.freeze({ _tag: 'UnionVariantIdentity', id: variant.canonical.id }), + ), + kind: 'Constructor', + label: variant.name.spelling, + detail: PresentationRenderer.unionVariant(union, variant), + sortGroup: 0, + }), + ], + ), + ) + const serviceCandidates = (service: DeclarationFacts.ServiceFact): ReadonlyArray => Object.freeze( service.operations.flatMap( @@ -701,6 +721,19 @@ export const complete = (options: { replacement: replacement.span, candidates: stable(enumCandidates(lookup.declaration)), }) + if (lookup?._tag === 'Resolved' && lookup.declaration._tag === 'UnionDeclaration') + return Object.freeze({ + _tag: 'CompletionResult', + context: Object.freeze({ + _tag: 'ActorMemberContext', + actor: + lookup.declaration.name._tag === 'Present' + ? lookup.declaration.name.spelling + : (qualifier ?? 'union'), + }), + replacement: replacement.span, + candidates: stable(unionCandidates(lookup.declaration)), + }) if ( lookup?._tag === 'Resolved' && (lookup.declaration._tag === 'StructDeclaration' || diff --git a/packages/compiler/src/DeclarationCompletion.ts b/packages/compiler/src/DeclarationCompletion.ts index ee1da334c..281e94ab8 100644 --- a/packages/compiler/src/DeclarationCompletion.ts +++ b/packages/compiler/src/DeclarationCompletion.ts @@ -1357,13 +1357,13 @@ export const complete = ( }) }) - const structs = modules.flatMap((module) => module.structs) + const aggregates = modules.flatMap((module) => [...module.structs, ...module.unions]) // One graph, two readers: the component walk and the self-edge test below must agree about what // "inline" means, or a struct that reaches itself through an indirection is a component of one // in the first and a cycle in the second. - const inlineParameters = inlineParametersOf(structs) + const inlineParameters = inlineParametersOf(aggregates) const cycleCause = new Map() - for (const component of stronglyConnected(structs, inlineParameters)) { + for (const component of stronglyConnected(aggregates, inlineParameters)) { const first = component.at(0) if (first === undefined) continue const keys = component.flatMap((struct) => @@ -1371,11 +1371,14 @@ export const complete = ( ) const selfEdge = keys.length === 1 && - first.fields.some((field) => + (first._tag === 'StructDeclaration' + ? first.fields + : first.variants.flatMap((variant) => variant.fields) + ).some((field) => inlineNeighbors(field, inlineParameters).some((neighbor) => neighbor === keys[0]), ) if (keys.length < 2 && !selfEdge) continue - const diagnostic = Diagnostic.inlineRecursiveStruct( + const diagnostic = Diagnostic.inlineRecursiveAggregate( Object.freeze(keys), first.name._tag === 'Present' ? first.name.token.span : first.syntax.span, ) @@ -1386,9 +1389,13 @@ export const complete = ( modules = modules.map((module): ModuleHeaders => { const members = module.members.map((member): MemberFact => { - if (member._tag !== 'StructDeclaration') return member + if (member._tag !== 'StructDeclaration' && member._tag !== 'UnionDeclaration') return member + const fields = + member._tag === 'StructDeclaration' + ? member.fields + : member.variants.flatMap((variant) => variant.fields) const dependencyMap = new Map() - for (const field of member.fields) { + for (const field of fields) { if (field.declaredType._tag === 'Resolved') { for (const type of Type.nominals(field.declaredType.type)) { dependencyMap.set(Type.key(type), type) @@ -1396,7 +1403,7 @@ export const complete = ( } } const dependencies = [...dependencyMap.values()].sort(Type.compare) - const fieldCause = member.fields.find( + const fieldCause = fields.find( (field) => (field.declaredType._tag === 'Unresolved' && field.declaredType.cause !== undefined) || (field.declaredType._tag === 'Resolved' && @@ -1411,13 +1418,26 @@ export const complete = ( fieldDependencyCause = fieldCause.declaredType.exposureCause } const cause = (key === undefined ? undefined : cycleCause.get(key)) ?? fieldDependencyCause + const dependency = Object.freeze( + cause === undefined + ? { _tag: 'Available' as const, types: Object.freeze(dependencies) } + : { _tag: 'Unavailable' as const, types: Object.freeze(dependencies), cause }, + ) + if (member._tag === 'UnionDeclaration' && cause !== undefined) + return Object.freeze({ + ...member, + dependency, + validity: Object.freeze({ + _tag: 'Invalid' as const, + causes: Object.freeze([ + ...(member.validity._tag === 'Invalid' ? member.validity.causes : []), + cause, + ]), + }), + }) return Object.freeze({ ...member, - dependency: Object.freeze( - cause === undefined - ? { _tag: 'Available', types: Object.freeze(dependencies) } - : { _tag: 'Unavailable', types: Object.freeze(dependencies), cause }, - ), + dependency, }) }) const moduleDiagnostics = diagnostics.filter( diff --git a/packages/compiler/src/DeclarationFacts.ts b/packages/compiler/src/DeclarationFacts.ts index 5f69a368b..a57a8b221 100644 --- a/packages/compiler/src/DeclarationFacts.ts +++ b/packages/compiler/src/DeclarationFacts.ts @@ -1743,6 +1743,25 @@ export const struct = (self: Index, module: string, name: string): StructLookup name, ) +export const unionByName = (self: Index, module: string, name: string): UnionLookup => { + const result = member(self, module, name) + if (result._tag === 'Missing') return result + if (result._tag === 'Resolved') + return result.declaration._tag === 'UnionDeclaration' + ? Object.freeze({ _tag: 'Resolved', spelling: name, declaration: result.declaration }) + : Object.freeze({ _tag: 'Missing', spelling: name }) + const declarations = result.declarations.filter( + (declaration): declaration is UnionFact => declaration._tag === 'UnionDeclaration', + ) + return declarations.length === 0 + ? Object.freeze({ _tag: 'Missing', spelling: name }) + : Object.freeze({ + _tag: 'Ambiguous', + spelling: name, + declarations: Object.freeze(declarations), + }) +} + /** Looks up one completed declaration by canonical identity. */ export const byCanonical = (self: Index, id: CanonicalId): MemberFact | undefined => { const result = member(self, id.module, id.name) diff --git a/packages/compiler/src/DeclarationResolution.ts b/packages/compiler/src/DeclarationResolution.ts index 7474d283c..4f81f1e00 100644 --- a/packages/compiler/src/DeclarationResolution.ts +++ b/packages/compiler/src/DeclarationResolution.ts @@ -25,6 +25,7 @@ import type { TypePathFact, TypeResolution, TypeResolver, + UnionFact, } from './DeclarationFacts.js' import { copyApplication, @@ -1793,25 +1794,34 @@ const inlineReach = ( * descents, so the sets only grow and the loop terminates. Because it is the least fixed point, * the answer does not depend on the order structs or modules arrive in. */ -export const inlineParametersOf = (structs: ReadonlyArray): InlineParameters => { - const declarations = new Map() - for (const struct of structs) - if (struct.canonical._tag === 'Canonical') - declarations.set(canonicalKey(struct.canonical.id), struct) +type InlineAggregateFact = StructFact | UnionFact + +const aggregateFields = (self: InlineAggregateFact): ReadonlyArray => + self._tag === 'StructDeclaration' + ? self.fields + : self.variants.flatMap((variant) => variant.fields) + +export const inlineParametersOf = ( + aggregates: ReadonlyArray, +): InlineParameters => { + const declarations = new Map() + for (const aggregate of aggregates) + if (aggregate.canonical._tag === 'Canonical') + declarations.set(canonicalKey(aggregate.canonical.id), aggregate) const inline = new Map>() for (const key of declarations.keys()) inline.set(key, new Set()) for (let growing = true; growing; ) { growing = false - for (const [key, struct] of declarations) { + for (const [key, aggregate] of declarations) { const reached = inline.get(key) - if (reached === undefined || struct.typeParameters.length === 0) continue + if (reached === undefined || aggregate.typeParameters.length === 0) continue // Keyed by position, matching how `TypeInference.substitution` binds arguments to parameters. const own = new Map( - struct.typeParameters.map( + aggregate.typeParameters.map( (parameter, position) => [Type.key(parameter.type), position] as const, ), ) - for (const field of struct.fields) { + for (const field of aggregateFields(aggregate)) { if (field.declaredType._tag !== 'Resolved') continue inlineReach(field.declaredType.type, inline, (member) => { if (!Type.isParameter(member)) return @@ -1841,11 +1851,11 @@ export const inlineNeighbors = ( } export const stronglyConnected = ( - structs: ReadonlyArray, + aggregates: ReadonlyArray, inlineParameters: InlineParameters, -): ReadonlyArray> => { - const canonical = structs - .filter((struct) => struct.canonical._tag === 'Canonical') +): ReadonlyArray> => { + const canonical = aggregates + .filter((aggregate) => aggregate.canonical._tag === 'Canonical') .sort((left, right) => { const leftId = left.canonical._tag === 'Canonical' ? left.canonical.id : undefined const rightId = right.canonical._tag === 'Canonical' ? right.canonical.id : undefined @@ -1854,16 +1864,16 @@ export const stronglyConnected = ( : canonicalKey(leftId).localeCompare(canonicalKey(rightId)) }) const byKey = new Map( - canonical.flatMap((struct) => - struct.canonical._tag === 'Canonical' - ? [[canonicalKey(struct.canonical.id), struct] as const] + canonical.flatMap((aggregate) => + aggregate.canonical._tag === 'Canonical' + ? [[canonicalKey(aggregate.canonical.id), aggregate] as const] : [], ), ) return Object.freeze( Graph.stronglyConnected(byKey.keys(), (key) => { - const struct = byKey.get(key) - return (struct?.fields ?? []) + const aggregate = byKey.get(key) + return (aggregate === undefined ? [] : aggregateFields(aggregate)) .flatMap((field) => inlineNeighbors(field, inlineParameters)) .filter((neighbor) => byKey.has(neighbor)) .sort() diff --git a/packages/compiler/src/Diagnostic.ts b/packages/compiler/src/Diagnostic.ts index 09c739835..d39f81b54 100644 --- a/packages/compiler/src/Diagnostic.ts +++ b/packages/compiler/src/Diagnostic.ts @@ -97,7 +97,7 @@ export const bindingConflictCode = 'SEM0016' as const export const duplicateFieldNameCode = 'SEM0017' as const export const expectedTypeCode = 'SEM0018' as const export const privateTypeExposureCode = 'SEM0019' as const -export const inlineRecursiveStructCode = 'SEM0020' as const +export const inlineRecursiveAggregateCode = 'SEM0020' as const export const inaccessibleStructConstructionCode = 'SEM0021' as const export const unknownStructFieldCode = 'SEM0022' as const export const duplicateStructInitializerCode = 'SEM0023' as const @@ -386,7 +386,7 @@ export type Code = | typeof duplicateFieldNameCode | typeof expectedTypeCode | typeof privateTypeExposureCode - | typeof inlineRecursiveStructCode + | typeof inlineRecursiveAggregateCode | typeof inaccessibleStructConstructionCode | typeof unknownStructFieldCode | typeof duplicateStructInitializerCode @@ -899,7 +899,7 @@ export type Reason = | { readonly _tag: 'IntegerPatternAgainstEnum'; readonly enum: string; readonly value: string } | { readonly _tag: 'ExpectedType'; readonly spelling: string } | { readonly _tag: 'PrivateTypeExposure'; readonly type: string } - | { readonly _tag: 'InlineRecursiveStruct'; readonly members: ReadonlyArray } + | { readonly _tag: 'InlineRecursiveAggregate'; readonly members: ReadonlyArray } | { readonly _tag: 'InaccessibleStructConstruction'; readonly type: string } | { readonly _tag: 'UnknownStructField'; readonly type: string; readonly field: string } | { @@ -1942,18 +1942,21 @@ export const privateTypeExposure = (type: string, span: SourceSpan.SourceSpan): span, }) -/** Creates the one canonical diagnostic for an inline recursive struct component. */ -export const inlineRecursiveStruct = ( +/** Creates the one canonical diagnostic for an inline recursive nominal-aggregate component. */ +export const inlineRecursiveAggregate = ( members: ReadonlyArray, span: SourceSpan.SourceSpan, ): Diagnostic => Object.freeze({ _tag: 'Diagnostic', phase: 'semantic', - code: inlineRecursiveStructCode, + code: inlineRecursiveAggregateCode, severity: 'error', - message: `Inline recursive struct layout: ${members.join(' -> ')}`, - reason: Object.freeze({ _tag: 'InlineRecursiveStruct', members: Object.freeze([...members]) }), + message: `Inline recursive aggregate layout: ${members.join(' -> ')}`, + reason: Object.freeze({ + _tag: 'InlineRecursiveAggregate', + members: Object.freeze([...members]), + }), span, }) diff --git a/packages/compiler/src/DocBlock.ts b/packages/compiler/src/DocBlock.ts index 97c9b4138..c6cd35e10 100644 --- a/packages/compiler/src/DocBlock.ts +++ b/packages/compiler/src/DocBlock.ts @@ -18,6 +18,7 @@ export interface DocBlock { const documentableKinds: ReadonlySet = new Set([ 'StructDeclaration', 'EnumDeclaration', + 'UnionDeclaration', 'ServiceDeclaration', 'InterfaceDeclaration', 'RoleDeclaration', @@ -26,6 +27,8 @@ const documentableKinds: ReadonlySet = new Set([ 'FunctionDeclaration', 'StructField', 'EnumMember', + 'UnionVariant', + 'UnionVariantField', 'TypeParameter', 'ParameterDeclaration', 'ServiceOperation', diff --git a/packages/compiler/src/ModuleSurface.ts b/packages/compiler/src/ModuleSurface.ts index dd071c567..da1a19ac4 100644 --- a/packages/compiler/src/ModuleSurface.ts +++ b/packages/compiler/src/ModuleSurface.ts @@ -1858,11 +1858,25 @@ const struct = (value: DeclarationFacts.StructFact): string => structDependency(value.dependency), ]) +const unionVariantCanonicalState = (value: DeclarationFacts.UnionVariantCanonicalState): string => { + switch (value._tag) { + case 'Canonical': + return record('CanonicalUnionVariant', [value.id.name]) + case 'Duplicate': + return record('DuplicateUnionVariant', [value.original.name]) + case 'Unidentified': + return record('UnidentifiedUnionVariant') + default: + return exhaustive(value) + } +} + const unionVariant = (value: DeclarationFacts.UnionVariantFact): string => record('UnionVariant', [ number(value.id.ordinal), - value.canonical._tag, + unionVariantCanonicalState(value.canonical), name(value.name), + value.kind, array(value.fields.map(field)), ]) @@ -1874,6 +1888,7 @@ const unionDeclaration = (value: DeclarationFacts.UnionFact): string => array(value.typeParameters.map(typeParameter)), name(value.name), array(value.variants.map(unionVariant)), + structDependency(value.dependency), value.validity._tag, ]) diff --git a/packages/compiler/src/Presentation.ts b/packages/compiler/src/Presentation.ts index 4a462b8ab..8882586b6 100644 --- a/packages/compiler/src/Presentation.ts +++ b/packages/compiler/src/Presentation.ts @@ -20,6 +20,7 @@ export type Presentation = }) | (Base & { readonly _tag: 'StructPresentation'; readonly name: string }) | (Base & { readonly _tag: 'UnionPresentation'; readonly name: string }) + | (Base & { readonly _tag: 'UnionVariantPresentation'; readonly name: string }) | (Base & { readonly _tag: 'EnumPresentation'; readonly name: string }) | (Base & { readonly _tag: 'EnumMemberPresentation'; readonly name: string }) | (Base & { readonly _tag: 'EnumOperationPresentation'; readonly name: string }) @@ -197,6 +198,34 @@ export const unionDeclaration = (self: DeclarationFacts.UnionFact): Presentation }) } +/** Renders one variant as a constructor of its complete nominal union parent. */ +export const unionVariant = ( + union: DeclarationFacts.UnionFact, + variant: DeclarationFacts.UnionVariantFact, +): Presentation => { + const unionName = union.name._tag === 'Present' ? union.name.spelling : '_' + const variantName = variant.name._tag === 'Present' ? variant.name.spelling : '_' + const typeParameters = + union.typeParameters.length === 0 + ? '' + : `<${union.typeParameters.map(typeParameterName).join(', ')}>` + const fields = + variant.kind === 'Unit' + ? '' + : ` { ${variant.fields + .map((field) => + field.name._tag === 'Present' + ? `${field.name.spelling}: ${declaredType(field.declaredType)}` + : `_: ${declaredType(field.declaredType)}`, + ) + .join(', ')} }` + return Object.freeze({ + _tag: 'UnionVariantPresentation', + name: variantName, + text: `${unionName}${typeParameters}.${variantName}${fields}: ${unionName}${typeParameters}`, + }) +} + /** Renders one nominal service contract without expanding its operation list. */ export const serviceDeclaration = ( self: DeclarationFacts.ServiceFact | DeclarationFacts.InterfaceFact, diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index a4d417420..a81fce33f 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -29,6 +29,10 @@ export type Identity = | { readonly _tag: 'BindingIdentity'; readonly id: Hir.BindingId } | { readonly _tag: 'PatternBindingIdentity'; readonly id: Match.BindingId } | { readonly _tag: 'FieldIdentity'; readonly id: DeclarationFacts.FieldId } + | { + readonly _tag: 'UnionVariantIdentity' + readonly id: DeclarationFacts.CanonicalUnionVariantId + } | { readonly _tag: 'EnumMemberIdentity'; readonly id: DeclarationFacts.CanonicalEnumMemberId } | { readonly _tag: 'EnumAssociatedOperationIdentity' @@ -147,6 +151,13 @@ const locationOfEnumMember = ( ? location(member.name.token.span.sourceId, member.syntax.span, member.name.token.span) : undefined +const locationOfUnionVariant = ( + variant: DeclarationFacts.UnionVariantFact, +): DeclarationLocation | undefined => + variant.name._tag === 'Present' + ? location(variant.name.token.span.sourceId, variant.syntax.span, variant.name.token.span) + : undefined + const locationOfField = ( index: DeclarationIndex.Index, field: DeclarationFacts.FieldFact, @@ -1204,10 +1215,31 @@ const collectMember = ( return } if (member._tag === 'RoleDeclaration') return - const fields = - member._tag === 'UnionDeclaration' - ? member.variants.flatMap((variant) => variant.fields) - : member.fields + if (member._tag === 'UnionDeclaration') { + for (const variant of member.variants) { + if (variant.name._tag === 'Present' && variant.canonical._tag === 'Canonical') + push( + pending, + variant.name.token.span, + 'Declaration', + available(Object.freeze({ _tag: 'UnionVariantIdentity', id: variant.canonical.id })), + locationOfUnionVariant(variant), + ) + for (const field of variant.fields) { + if (field.name._tag === 'Present') + push( + pending, + field.name.token.span, + 'Declaration', + available(Object.freeze({ _tag: 'FieldIdentity', id: field.id })), + locationOfField(index, field), + ) + collectDeclaredType(field.declaredType, index, scope, pending) + } + } + return + } + const fields = member.fields for (const field of fields) { if (field.name._tag === 'Present') push( @@ -1445,6 +1477,8 @@ export const identityKey = (identity: Identity): string => { return `pattern:${JSON.stringify(identity.id)}` case 'FieldIdentity': return `field:${DeclarationFacts.fieldIdKey(identity.id)}` + case 'UnionVariantIdentity': + return `union-variant:${identity.id.union.module}.${identity.id.union.name}.${identity.id.name}` case 'EnumMemberIdentity': return `enum-member:${identity.id.enum.module}.${identity.id.enum.name}.${identity.id.name}` case 'EnumAssociatedOperationIdentity': diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 3ebafd16f..e8e8dcbed 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'cff32c3cd19cd28ff1e92d72c372f0a081f5fb0f0fb0ae1a0456066a36247cca' +export const compilerDigest = 'b30e71f5b07c99ea41f34988e217d56f86ec98e04137a04e9a9024c2dd700a28' diff --git a/packages/compiler/test/DeclarationIndex.test.ts b/packages/compiler/test/DeclarationIndex.test.ts index b67ce66d4..7e028980e 100644 --- a/packages/compiler/test/DeclarationIndex.test.ts +++ b/packages/compiler/test/DeclarationIndex.test.ts @@ -1063,6 +1063,19 @@ it.effect('diagnoses private exposure and inline recursive struct components can ['Hidden', 'Hidden', 'Hidden'], ) + const exposedUnion = yield* collect('union-exposure', [ + [ + 'union-exposure', + 'struct Hidden {}\npub union Public { Value { pub visible: Hidden, private: Hidden } }', + ], + ]) + assert.deepEqual( + exposedUnion.diagnostics.map((diagnostic) => diagnostic.code), + ['SEM0019'], + ) + assert.strictEqual(exposedUnion.modules.at(0)?.unions.at(0)?.validity._tag, 'Invalid') + assert.strictEqual(exposedUnion.modules.at(0)?.unions.at(0)?.dependency._tag, 'Unavailable') + const recursiveSource = 'import b.B\npub struct A { value: B.B }' const recursive = yield* collect('a/A', [ ['a/A', recursiveSource], @@ -1090,6 +1103,16 @@ it.effect('diagnoses private exposure and inline recursive struct components can ['SEM0020'], ) assert.strictEqual(direct.modules.at(0)?.structs.at(0)?.dependency._tag, 'Unavailable') + + const mixed = yield* collect('mixed', [ + ['mixed', 'union Link { Next { node: Node }, End }\nstruct Node { link: Link }'], + ]) + assert.deepEqual( + mixed.diagnostics.map((diagnostic) => diagnostic.code), + ['SEM0020'], + ) + assert.strictEqual(mixed.modules.at(0)?.unions.at(0)?.dependency._tag, 'Unavailable') + assert.strictEqual(mixed.modules.at(0)?.structs.at(0)?.dependency._tag, 'Unavailable') }), ) diff --git a/packages/compiler/test/EditorIntelligence.test.ts b/packages/compiler/test/EditorIntelligence.test.ts index 5ff2f5a82..475afcb7a 100644 --- a/packages/compiler/test/EditorIntelligence.test.ts +++ b/packages/compiler/test/EditorIntelligence.test.ts @@ -109,6 +109,77 @@ pub fn main() -> i32 { return 0 }` ) }) +it.effect('presents nominal unions, variants, and variant fields from canonical facts', () => { + const source = `/// A computation outcome. +pub union Result { + /// A successful payload. + Success { pub value: A }, + Failure { pub error: E }, +} +pub fn main() -> i32 { return 0 }` + return Analysis.ofSource('main', encoder.encode(source)).pipe( + Effect.map((snapshot) => { + const declaration = occurrenceAt(snapshot, source, 'Result') + const variant = occurrenceAt(snapshot, source, 'Success') + const field = occurrenceAt(snapshot, source, 'value') + const union = Analysis.unionByName(snapshot, 'main', 'Result') + + assert.strictEqual(declaration?.role, 'Declaration') + assert.strictEqual(variant?.role, 'Declaration') + assert.strictEqual(field?.role, 'Declaration') + assert.strictEqual(variant?.resolution._tag, 'Available') + assert.strictEqual( + declaration === undefined + ? undefined + : Analysis.occurrencePresentation(snapshot, 'main', declaration)?.text, + 'pub union Result', + ) + assert.strictEqual( + variant === undefined + ? undefined + : Analysis.occurrencePresentation(snapshot, 'main', variant)?.text, + 'Result.Success { value: A }: Result', + ) + assert.strictEqual( + documentationText( + snapshot, + Analysis.documentationAt(snapshot, 'main', source.indexOf('Success')), + ), + '/// A successful payload.', + ) + assert.strictEqual(union._tag, 'Resolved') + if (union._tag !== 'Resolved') return undefined + const selected = Analysis.unionVariantByName(union.declaration, 'Success') + assert.strictEqual(selected._tag, 'Resolved') + if (selected._tag !== 'Resolved') return undefined + assert.strictEqual( + Analysis.unionVariantFieldByName(selected.variant, 'value')._tag, + 'Resolved', + ) + return undefined + }), + ) +}) + +it.effect('completes variants from a nominal union qualifier', () => { + const source = `union State { Ready, Waiting { count: i32 } } +pub fn main() -> i32 { let state = State. return 0 }` + return Analysis.ofSource('main', encoder.encode(source)).pipe( + Effect.map((snapshot) => { + const offset = source.indexOf('State.') + 'State.'.length + const completion = Analysis.completionAt(snapshot, 'main', offset) + assert.deepEqual( + completion?.candidates.map((candidate) => [candidate.label, candidate.kind]), + [ + ['Ready', 'Constructor'], + ['Waiting', 'Constructor'], + ], + ) + return undefined + }), + ) +}) + it.effect('answers raw documentation for modules, declarations, children, and references', () => { const source = `//! Recovery module. /// A recoverable problem. diff --git a/packages/compiler/test/ModuleSurface.test.ts b/packages/compiler/test/ModuleSurface.test.ts index 0c8414c1e..31d2bd03b 100644 --- a/packages/compiler/test/ModuleSurface.test.ts +++ b/packages/compiler/test/ModuleSurface.test.ts @@ -88,6 +88,38 @@ it.effect('projects scalar enum declarations deterministically into module surfa }), ) +it.effect('projects complete nominal union headers into deterministic module surfaces', () => + Effect.gen(function* () { + const base = yield* surface( + 'pub union Result { Success { pub value: A }, Failure { error: E }, Pending }', + ) + const repeated = yield* surface( + 'pub union Result { Success { pub value: A }, Failure { error: E }, Pending }', + ) + const reordered = yield* surface( + 'pub union Result { Failure { error: E }, Success { pub value: A }, Pending }', + ) + const changedField = yield* surface( + 'pub union Result { Success { pub value: E }, Failure { error: E }, Pending }', + ) + const changedVisibility = yield* surface( + 'pub union Result { Success { value: A }, Failure { error: E }, Pending }', + ) + const unitInstead = yield* surface( + 'pub union Result { Success, Failure { error: E }, Pending }', + ) + + assert.strictEqual(ModuleSurface.equals(base, repeated), true) + assert.strictEqual(ModuleSurface.equals(base, reordered), false) + assert.strictEqual(ModuleSurface.equals(base, changedField), false) + assert.strictEqual(ModuleSurface.equals(base, changedVisibility), false) + assert.strictEqual(ModuleSurface.equals(base, unitInstead), false) + assert.include(base.canonical, 'UnionDeclaration') + assert.include(base.canonical, 'CanonicalUnionVariant') + assert.include(base.canonical, 'AvailableStructDependency') + }), +) + it.effect('compares independently allocated equal facts exactly', () => Effect.gen(function* () { const source = `pub fn answer(value: i32) -> i32 { return value } From 22b06b3926d448752628509bd7fc3b8b151574fd Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 13:32:57 -0300 Subject: [PATCH 05/42] feat(compiler): elaborate nominal union constructors --- apps/docs/content/language/diagnostics.md | 7 +- openspec/changes/add-nominal-unions/tasks.md | 8 +- .../compiler/src/DeclarationResolution.ts | 3 +- packages/compiler/src/Diagnostic.ts | 59 +++ packages/compiler/src/Elaboration.ts | 27 ++ packages/compiler/src/ExpressionAnalysis.ts | 360 +++++++++++++++--- packages/compiler/src/HirLowering.ts | 4 + packages/compiler/src/Ownership.ts | 4 +- packages/compiler/src/SemanticOccurrence.ts | 31 ++ .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/test/StructValues.test.ts | 102 +++++ 11 files changed, 549 insertions(+), 58 deletions(-) diff --git a/apps/docs/content/language/diagnostics.md b/apps/docs/content/language/diagnostics.md index 8e66799bc..3ae9998fc 100644 --- a/apps/docs/content/language/diagnostics.md +++ b/apps/docs/content/language/diagnostics.md @@ -16,11 +16,11 @@ $ pnpm --filter @silklang/compiler documentation:generate | `LEX` | Lexical | 7 | | `PAR` | Parser | 4 | | `MOD` | Module | 3 | -| `SEM` | Semantic | 158 | +| `SEM` | Semantic | 161 | | `OWN` | Ownership | 16 | | `LAY` | Layout | 1 | -There are 189 codes in total. +There are 192 codes in total. ## Lexical (`LEX`) @@ -213,6 +213,9 @@ There are 189 codes in total. | `SEM0164` | Stable code for a nominal union declaration with no variants. | `Union must declare at least one variant` | | `SEM0165` | Stable code for a repeated variant name within one nominal union. | `Duplicate union variant ` | | `SEM0166` | Stable code for a named-field variant whose braces contain no field. | `Union variant must omit braces or declare at least one field` | +| `SEM0167` | Stable code for a variant selector absent from its resolved nominal union. | `Union has no variant ` | +| `SEM0168` | Stable code for a variant qualifier that does not name a nominal union. | `Expected a nominal union, found ` | +| `SEM0169` | Stable code for construction through an incomplete nominal union declaration. | `Cannot construct invalid nominal union ` | ## Ownership (`OWN`) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 5080eaff5..ea1185ae5 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -17,10 +17,10 @@ ## 3. Type Application and Variant Construction -- [ ] 3.1 Teach nominal-type lookup and substitution to distinguish union declarations while keeping the complete parent application as the only value type, and verify no variant type or structural member is created. -- [ ] 3.2 Refactor struct-literal field checking into a shared aggregate-field elaborator without changing existing struct facts or diagnostics, and verify the existing struct construction and generic-inference suites remain byte-for-byte stable where golden data exists. -- [ ] 3.3 Implement two-stage variant constructor resolution—parent declaration and explicit prefix first, field-only suffix inference second—and verify zero-prefix, partial-prefix, conflicting, and parent-only uninferred argument cases. -- [ ] 3.4 Implement unit and named-field construction with complete field initialization, construction authority, visibility fences, type compatibility, represented fields, and precise parent result types, and verify cross-module private fields block raw construction. +- [x] 3.1 Teach nominal-type lookup and substitution to distinguish union declarations while keeping the complete parent application as the only value type, and verify no variant type or structural member is created. +- [x] 3.2 Refactor struct-literal field checking into a shared aggregate-field elaborator without changing existing struct facts or diagnostics, and verify the existing struct construction and generic-inference suites remain byte-for-byte stable where golden data exists. +- [x] 3.3 Implement two-stage variant constructor resolution—parent declaration and explicit prefix first, field-only suffix inference second—and verify zero-prefix, partial-prefix, conflicting, and parent-only uninferred argument cases. +- [x] 3.4 Implement unit and named-field construction with complete field initialization, construction authority, visibility fences, type compatibility, represented fields, and precise parent result types, and verify cross-module private fields block raw construction. - [ ] 3.5 Preserve every variant through generic specialization, including equal and `never` payloads while independently renormalizing structural-union fields, and verify specialization facts never collapse or flatten variants. - [ ] 3.6 Reject direct parent-union field projection and common-field synthesis while retaining diagnostic facts, and verify `result.value` is unavailable until a variant pattern binds its payload. - [ ] 3.7 Admit interface, operator, Copy, and Drop declarations against nominal union parents through the ordinary conformance/coherence path, and verify variant names do not become lookup or implementation targets. diff --git a/packages/compiler/src/DeclarationResolution.ts b/packages/compiler/src/DeclarationResolution.ts index 4f81f1e00..b7bb8244d 100644 --- a/packages/compiler/src/DeclarationResolution.ts +++ b/packages/compiler/src/DeclarationResolution.ts @@ -834,10 +834,11 @@ export const canonicalKey = (id: CanonicalId): string => `${id.module}.${id.name export const memberByNominal = ( modules: ReadonlyArray, type: Type.Nominal, -): StructFact | ServiceFact | InterfaceFact | undefined => { +): StructFact | UnionFact | ServiceFact | InterfaceFact | undefined => { const module = modules.find((candidate) => candidate.module === type.module) return [ ...(module?.structs ?? []), + ...(module?.unions ?? []), ...(module?.services ?? []), ...(module?.interfaces ?? []), ].find( diff --git a/packages/compiler/src/Diagnostic.ts b/packages/compiler/src/Diagnostic.ts index d39f81b54..8093e654b 100644 --- a/packages/compiler/src/Diagnostic.ts +++ b/packages/compiler/src/Diagnostic.ts @@ -324,6 +324,12 @@ export const emptyNominalUnionCode = 'SEM0164' as const export const duplicateUnionVariantCode = 'SEM0165' as const /** Stable code for a named-field variant whose braces contain no field. */ export const emptyUnionVariantCode = 'SEM0166' as const +/** Stable code for a variant selector absent from its resolved nominal union. */ +export const unknownUnionVariantCode = 'SEM0167' as const +/** Stable code for a variant qualifier that does not name a nominal union. */ +export const expectedNominalUnionCode = 'SEM0168' as const +/** Stable code for construction through an incomplete nominal union declaration. */ +export const invalidNominalUnionConstructionCode = 'SEM0169' as const /** Stable code for a use of a binding after its consuming move. */ export const useAfterMoveCode = 'OWN0001' as const @@ -526,6 +532,9 @@ export type Code = | typeof emptyNominalUnionCode | typeof duplicateUnionVariantCode | typeof emptyUnionVariantCode + | typeof unknownUnionVariantCode + | typeof expectedNominalUnionCode + | typeof invalidNominalUnionConstructionCode | typeof useAfterMoveCode | typeof partialMoveCode | typeof explicitMoveRequiredCode @@ -590,6 +599,9 @@ export type Reason = readonly originalSpan: SourceSpan.SourceSpan } | { readonly _tag: 'EmptyUnionVariant'; readonly variant: string } + | { readonly _tag: 'UnknownUnionVariant'; readonly union: string; readonly variant: string } + | { readonly _tag: 'ExpectedNominalUnion'; readonly actual: string } + | { readonly _tag: 'InvalidNominalUnionConstruction'; readonly union: string } | { readonly _tag: 'UnknownModule'; readonly module: string } | { readonly _tag: 'SelfImport'; readonly module: string } | { readonly _tag: 'ReservedModuleIdentity'; readonly module: string } @@ -1717,6 +1729,53 @@ export const emptyUnionVariant = (variantName: string, span: SourceSpan.SourceSp span, }) +/** Creates the diagnostic for selecting a missing variant from a resolved nominal union. */ +export const unknownUnionVariant = ( + unionName: string, + variantName: string, + span: SourceSpan.SourceSpan, +): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: unknownUnionVariantCode, + severity: 'error', + message: `Union ${unionName} has no variant ${variantName}`, + reason: Object.freeze({ + _tag: 'UnknownUnionVariant', + union: unionName, + variant: variantName, + }), + span, + }) + +/** Creates the diagnostic for a variant qualifier that is not a nominal union. */ +export const expectedNominalUnion = (actual: string, span: SourceSpan.SourceSpan): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: expectedNominalUnionCode, + severity: 'error', + message: `Expected a nominal union, found ${actual}`, + reason: Object.freeze({ _tag: 'ExpectedNominalUnion', actual }), + span, + }) + +/** Creates the construction fence for a nominal union with invalid declaration facts. */ +export const invalidNominalUnionConstruction = ( + unionName: string, + span: SourceSpan.SourceSpan, +): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: invalidNominalUnionConstructionCode, + severity: 'error', + message: `Cannot construct invalid nominal union ${unionName}`, + reason: Object.freeze({ _tag: 'InvalidNominalUnionConstruction', union: unionName }), + span, + }) + export const unsupportedEnumRepresentation = ( spelling: string, allowed: ReadonlyArray, diff --git a/packages/compiler/src/Elaboration.ts b/packages/compiler/src/Elaboration.ts index 850b65c31..b8a454a17 100644 --- a/packages/compiler/src/Elaboration.ts +++ b/packages/compiler/src/Elaboration.ts @@ -499,6 +499,16 @@ export type StructTargetFact = } | { readonly _tag: 'Unavailable'; readonly cause?: Diagnostic.Identity } +export type UnionVariantTargetFact = + | { + readonly _tag: 'Resolved' + readonly union: DeclarationFacts.UnionFact + readonly variant: DeclarationFacts.UnionVariantFact + readonly type: Type.Nominal + readonly token: Token.Token + } + | { readonly _tag: 'Unavailable'; readonly cause?: Diagnostic.Identity } + export type StructInitializerState = | { readonly _tag: 'Resolved'; readonly field: DeclarationFacts.FieldFact } | { readonly _tag: 'Unknown'; readonly cause: Diagnostic.Identity } @@ -549,6 +559,20 @@ export interface StructLiteralExpressionFact { readonly syntax: SyntaxTree.Node } +export interface UnionVariantExpressionFact { + readonly _tag: 'UnionVariant' + readonly target: UnionVariantTargetFact + readonly authorized: boolean + readonly typeArguments: ReadonlyArray + readonly initializers: ReadonlyArray + readonly fields: ReadonlyArray<{ + readonly field: DeclarationFacts.FieldFact + readonly initializer: StructInitializerFact + }> + readonly type: ExpressionTypeFact + readonly syntax: SyntaxTree.Node +} + export type ProjectionState = | { readonly _tag: 'Resolved'; readonly field: DeclarationFacts.FieldFact } | { readonly _tag: 'SliceLength' } @@ -875,6 +899,7 @@ export type ExpressionFact = | BorrowExpressionFact | MatchExpressionFact | StructLiteralExpressionFact + | UnionVariantExpressionFact | ArrayLiteralExpressionFact | FieldProjectionExpressionFact | IndexProjectionExpressionFact @@ -1383,6 +1408,7 @@ export const expressionNodeKinds: ReadonlyArray = Object.fr 'BorrowExpression', 'MatchExpression', 'StructLiteralExpression', + 'UnionVariantExpression', 'ArrayLiteralExpression', 'FieldProjectionExpression', 'IndexProjectionExpression', @@ -1406,6 +1432,7 @@ export const isRecursiveArgumentNode = (element: SyntaxTree.Element): element is element.kind === 'BorrowExpression' || element.kind === 'MatchExpression' || element.kind === 'StructLiteralExpression' || + element.kind === 'UnionVariantExpression' || element.kind === 'ArrayLiteralExpression' || element.kind === 'FieldProjectionExpression' || element.kind === 'IndexProjectionExpression' || diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index 5135fb113..c369cb50f 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -51,6 +51,7 @@ import type { StructInitializerState, StructTargetFact, StructTypeArgumentFact, + UnionVariantTargetFact, } from './Elaboration.js' import { argumentFact, @@ -466,7 +467,13 @@ export const analyzeEnumMember = ( resolution: ResolutionContext, expected?: SemanticType, ): ExpressionResult | undefined => { - const identifiers = SyntaxTree.tokens(node).filter((token) => token.kind === 'Identifier') + const path = SyntaxTree.directNode(node, 'TypePath') + const pathIdentifiers = + path === undefined ? [] : SyntaxTree.tokens(path).filter((token) => token.kind === 'Identifier') + const identifiers = + pathIdentifiers.length === 2 + ? pathIdentifiers + : SyntaxTree.tokens(node).filter((token) => token.kind === 'Identifier') const qualifierToken = identifiers.at(0) const memberToken = identifiers.at(1) if (qualifierToken === undefined || memberToken === undefined || identifiers.length !== 2) @@ -1011,6 +1018,11 @@ export interface StructTargetResult { readonly diagnostics: ReadonlyArray } +export interface UnionVariantTargetResult { + readonly fact: UnionVariantTargetFact + readonly diagnostics: ReadonlyArray +} + export const intrinsicStruct = ( type: Type.Nominal, syntax: SyntaxTree.Node, @@ -1242,6 +1254,203 @@ export const resolveStructTarget = ( }) } +const unavailableUnionVariantTarget = ( + diagnostic: Diagnostic.Diagnostic, + diagnostics: ReadonlyArray = [], +): UnionVariantTargetResult => + Object.freeze({ + fact: Object.freeze({ _tag: 'Unavailable', cause: Diagnostic.identity(diagnostic) }), + diagnostics: Diagnostic.merge(diagnostics, [diagnostic]), + }) + +const selectedUnionVariant = ( + union: DeclarationFacts.UnionFact, + type: Type.Nominal, + variantName: string, + token: Token.Token, + diagnostics: ReadonlyArray, +): UnionVariantTargetResult => { + const variant = DeclarationFacts.lookupUnionVariant(union.variants, variantName) + if (variant._tag !== 'Resolved') + return unavailableUnionVariantTarget( + Diagnostic.unknownUnionVariant(Type.encode(type), variantName, token.span), + diagnostics, + ) + if (union.validity._tag !== 'Valid') + return unavailableUnionVariantTarget( + Diagnostic.invalidNominalUnionConstruction(Type.encode(type), token.span), + diagnostics, + ) + return Object.freeze({ + fact: Object.freeze({ _tag: 'Resolved', union, variant: variant.variant, type, token }), + diagnostics: Object.freeze(diagnostics), + }) +} + +export const resolveUnionVariantTarget = ( + source: SourceFile.SourceFile, + selector: SyntaxTree.Node, + resolution: ResolutionContext, + caller?: DeclarationFact, +): UnionVariantTargetResult => { + const parentSyntax = + SyntaxTree.directNode(selector, 'AppliedType') ?? childNode(selector, 'TypePath') + const variantToken = SyntaxTree.tokens(selector) + .filter((token) => token.kind === 'Identifier') + .at(-1) + if (parentSyntax === undefined || variantToken === undefined) { + const diagnostic = Diagnostic.expectedNominalUnion('unavailable selector', selector.span) + return unavailableUnionVariantTarget(diagnostic) + } + const environment = new Map( + (caller?.typeParameters ?? []).flatMap((parameter) => + parameter.name._tag === 'Present' ? [[parameter.name.spelling, parameter.type] as const] : [], + ), + ) + const analyzed = DeclarationCollection.analyzeDeclaredType(source, parentSyntax, environment) + const nameResolution: NameResolution.Resolution = Object.freeze({ + _tag: 'NameResolution', + modules: Object.freeze([resolution.scope]), + diagnostics: Object.freeze([]), + }) + const applied = analyzed.fact._tag === 'Applied' ? analyzed.fact : undefined + const targetFact = applied?.target ?? analyzed.fact + const path = targetFact._tag === 'Unresolved' ? targetFact.path : undefined + let base: Type.Nominal | undefined + if (path === undefined) { + if (targetFact._tag === 'Resolved' && Type.isNominal(targetFact.type)) base = targetFact.type + } else { + const candidate = NameResolution.resolveType( + nameResolution, + resolution.index, + source.id, + path, + ).fact + if (candidate._tag === 'Resolved' && Type.isNominal(candidate.type)) base = candidate.type + } + const declaration = + base === undefined + ? undefined + : DeclarationFacts.byCanonical(resolution.index, { + _tag: 'CanonicalDeclarationId', + module: base.module, + name: base.name, + }) + if (base === undefined || declaration?._tag !== 'UnionDeclaration') { + const diagnostic = Diagnostic.expectedNominalUnion( + base === undefined ? 'unavailable type' : Type.encode(base), + parentSyntax.span, + ) + return unavailableUnionVariantTarget(diagnostic, analyzed.diagnostics) + } + const supplied = applied?.arguments ?? [] + const sourceParameters = declaration.typeParameters.filter( + (parameter) => + parameter.type.kind !== 'CallableRepresentation' && + parameter.type.kind !== 'EffectRepresentation', + ) + if (supplied.length > sourceParameters.length) { + const diagnostic = Diagnostic.expectedNominalUnion(Type.encode(base), parentSyntax.span) + return unavailableUnionVariantTarget(diagnostic, analyzed.diagnostics) + } + const resolvedArguments = supplied.map((argument) => + DeclarationResolution.resolveTypeFact( + resolution.index, + source.id, + argument, + (module, argumentPath) => + NameResolution.resolveType(nameResolution, resolution.index, module, argumentPath), + ), + ) + let suppliedOrdinal = 0 + const arguments_ = declaration.typeParameters.flatMap( + (parameter): ReadonlyArray => { + if ( + parameter.type.kind === 'CallableRepresentation' || + parameter.type.kind === 'EffectRepresentation' + ) + return [Type.representationParameterArgument(parameter.type)] + const resolved = resolvedArguments.at(suppliedOrdinal) + suppliedOrdinal += 1 + if (resolved === undefined) return [Type.parameterArgument(parameter.type)] + if (resolved.fact._tag !== 'Resolved') return [] + if (parameter.type.kind === 'Value') + return Type.isTypeArgument(resolved.fact.type) ? [resolved.fact.type] : [] + if ( + parameter.type.kind === 'RequirementRow' && + Type.isParameter(resolved.fact.type) && + resolved.fact.type.kind === 'RequirementRow' + ) + return [Type.requirementRowArgument([], [resolved.fact.type])] + return [] + }, + ) + if ( + arguments_.length !== declaration.typeParameters.length || + TypeInference.prefixSubstitution( + declaration.typeParameters.map((parameter) => parameter.type), + arguments_, + ) === undefined + ) { + const diagnostic = Diagnostic.expectedNominalUnion(Type.encode(base), parentSyntax.span) + return unavailableUnionVariantTarget( + diagnostic, + Diagnostic.merge( + analyzed.diagnostics, + ...resolvedArguments.map((argument) => argument.diagnostics), + ), + ) + } + return selectedUnionVariant( + declaration, + Type.specializeNominal(base, arguments_), + spelling(source, variantToken), + variantToken, + Diagnostic.merge( + analyzed.diagnostics, + ...resolvedArguments.map((argument) => argument.diagnostics), + ), + ) +} + +const resolveBareUnionVariantTarget = ( + source: SourceFile.SourceFile, + node: SyntaxTree.Node, + resolution: ResolutionContext, +): UnionVariantTargetResult | undefined => { + const tokens = SyntaxTree.tokens(node) + const initializerStart = tokens.findIndex((token) => token.kind === 'LeftBrace') + const selectorTokens = initializerStart === -1 ? tokens : tokens.slice(0, initializerStart) + const identifiers = selectorTokens.filter((token) => token.kind === 'Identifier') + const parentToken = identifiers.at(0) + const variantToken = identifiers.at(1) + if (identifiers.length !== 2 || parentToken === undefined || variantToken === undefined) + return undefined + const lookup = NameResolution.lookup( + resolution.scope, + resolution.index, + spelling(source, parentToken), + ) + if (lookup._tag !== 'Resolved' || lookup.declaration._tag !== 'UnionDeclaration') return undefined + const union = lookup.declaration + const type = + union.canonical._tag === 'Canonical' + ? Type.nominal( + union.canonical.id.module, + union.canonical.id.name, + union.typeParameters.map((parameter) => Type.parameterArgument(parameter.type)), + ) + : undefined + if (type === undefined) { + const diagnostic = Diagnostic.invalidNominalUnionConstruction( + spelling(source, parentToken), + parentToken.span, + ) + return unavailableUnionVariantTarget(diagnostic) + } + return selectedUnionVariant(union, type, spelling(source, variantToken), variantToken, []) +} + export interface PatternCounters { pattern: number binding: number @@ -2352,7 +2561,7 @@ export const isOwnStructArgument = ( argument: Type.GenericArgument, ): boolean => Type.equalsGenericArgument(Type.parameterArgument(parameter), argument) -export const analyzeStructLiteral = ( +export const analyzeAggregateLiteral = ( source: SourceFile.SourceFile, node: SyntaxTree.Node, declarations: ReadonlyArray, @@ -2360,17 +2569,40 @@ export const analyzeStructLiteral = ( scope: Scope, resolution: ResolutionContext, ): ExpressionResult => { - const targetSyntax = SyntaxTree.directNode(node, 'AppliedType') ?? childNode(node, 'TypePath') - const target = resolveStructTarget(source, targetSyntax, resolution, declaration, true) + const selector = SyntaxTree.directNode(node, 'UnionVariantSelector') + const unionTarget = + selector === undefined + ? resolveBareUnionVariantTarget(source, node, resolution) + : resolveUnionVariantTarget(source, selector, resolution, declaration) + let targetSyntax: SyntaxTree.Node + if (selector !== undefined) { + targetSyntax = SyntaxTree.directNode(selector, 'AppliedType') ?? childNode(selector, 'TypePath') + } else if (unionTarget !== undefined && node.kind === 'FieldProjectionExpression') { + targetSyntax = node + } else { + targetSyntax = SyntaxTree.directNode(node, 'AppliedType') ?? childNode(node, 'TypePath') + } + const target = + unionTarget ?? resolveStructTarget(source, targetSyntax, resolution, declaration, true) const diagnostics: Array = [...target.diagnostics] - const struct = target.fact._tag === 'Resolved' ? target.fact.struct : undefined + let aggregate: DeclarationFacts.StructFact | DeclarationFacts.UnionFact | undefined + let aggregateFields: ReadonlyArray = Object.freeze([]) + if (target.fact._tag === 'Resolved') { + if ('union' in target.fact) { + aggregate = target.fact.union + aggregateFields = target.fact.variant.fields + } else { + aggregate = target.fact.struct + aggregateFields = target.fact.struct.fields + } + } const nominal = target.fact._tag === 'Resolved' ? target.fact.type : undefined - const nominalLabel = nominal === undefined ? 'unknown struct' : Type.encode(nominal) + const nominalLabel = nominal === undefined ? 'unknown aggregate' : Type.encode(nominal) const inferredArguments = new Map() const argumentOrigins = new Map>() const explicitArguments = new Set() - if (struct !== undefined && nominal !== undefined) { - for (const [ordinal, parameter] of struct.typeParameters.entries()) { + if (aggregate !== undefined && nominal !== undefined) { + for (const [ordinal, parameter] of aggregate.typeParameters.entries()) { const argument = nominal.arguments.at(ordinal) if (argument === undefined || isOwnStructArgument(parameter.type, argument)) continue const parameterKey = Type.key(parameter.type) @@ -2385,8 +2617,8 @@ export const analyzeStructLiteral = ( const definingModule = nominal?.module const authorized = definingModule !== undefined && - struct?.syntax.kind === 'StructDeclaration' && - struct.fields.every((field) => field.visibility === 'Public' || definingModule === source.id) + aggregate !== undefined && + aggregateFields.every((field) => field.visibility === 'Public' || definingModule === source.id) const accessDiagnostic = nominal !== undefined && !authorized ? Diagnostic.inaccessibleStructConstruction(Type.encode(nominal), node.span) @@ -2399,15 +2631,17 @@ export const analyzeStructLiteral = ( const nameToken = directToken(initializer, 'Identifier') const name = nameToken === undefined ? undefined : spelling(source, nameToken) const fieldLookup = - struct === undefined || name === undefined + aggregate === undefined || name === undefined ? undefined - : DeclarationFacts.lookupField(struct.fields, name) - const expected = + : DeclarationFacts.lookupField(aggregateFields, name) + const contextualFieldType = fieldLookup?._tag === 'Resolved' && fieldLookup.field.declaredType._tag === 'Resolved' ? Type.substitute(fieldLookup.field.declaredType.type, structSubstitution) : undefined const contextualExpected = - expected !== undefined && Type.isRepresented(expected) ? expected.contract : expected + contextualFieldType !== undefined && Type.isRepresented(contextualFieldType) + ? contextualFieldType.contract + : contextualFieldType const expressionNode = initializer.children.find(isExpressionNode) if (expressionNode === undefined) { throw new RangeError('Struct initializer requires an expression node') @@ -2426,7 +2660,7 @@ export const analyzeStructLiteral = ( } diagnostics.push(...expression.diagnostics) let state: StructInitializerState = Object.freeze({ _tag: 'Unavailable' }) - if (name !== undefined && nameToken !== undefined && struct !== undefined) { + if (name !== undefined && nameToken !== undefined && aggregate !== undefined) { const previous = seen.get(name) if (fieldLookup?._tag !== 'Resolved') { const diagnostic = Diagnostic.unknownStructField(nominalLabel, name, nameToken.span) @@ -2459,10 +2693,7 @@ export const analyzeStructLiteral = ( fieldLookup.field.declaredType._tag === 'Resolved' && expression.type !== undefined ) { - const expectedType = Type.substitute( - fieldLookup.field.declaredType.type, - structSubstitution, - ) + const expectedType = fieldLookup.field.declaredType.type const expectedValue = Type.isRepresented(expectedType) ? expectedType.contract : expectedType @@ -2478,7 +2709,7 @@ export const analyzeStructLiteral = ( if (TypeInference.infer(expectedType.contract, actualValue, candidateSubstitution)) { const siteSubstitution = new Map() TypeInference.infer(expectedType.contract, actualValue, siteSubstitution) - for (const parameter of struct.typeParameters) { + for (const parameter of aggregate.typeParameters) { if ( parameter.type.kind === 'CallableRepresentation' || parameter.type.kind === 'EffectRepresentation' @@ -2542,7 +2773,7 @@ export const analyzeStructLiteral = ( actualRepresentation, ) if (represented.representation.admissibility._tag === 'Unavailable') { - const requiredParameter = struct.typeParameters.find( + const requiredParameter = aggregate.typeParameters.find( (candidate) => Type.key(candidate.type) === Type.key(parameter), ) const actualParameter = @@ -2605,7 +2836,7 @@ export const analyzeStructLiteral = ( if (!TypeInference.infer(expectedType, expression.type, candidateSubstitution)) { const impliedSubstitution = new Map() if (TypeInference.infer(expectedType, expression.type, impliedSubstitution)) { - for (const parameter of struct.typeParameters) { + for (const parameter of aggregate.typeParameters) { if (parameter.type.kind !== 'Value') continue const parameterKey = Type.key(parameter.type) const previous = inferredArguments.get(parameterKey) @@ -2633,7 +2864,7 @@ export const analyzeStructLiteral = ( expression.type, ) if (representationDiagnostic === undefined && divergence !== undefined) { - const parameter = struct.typeParameters.find((candidate) => { + const parameter = aggregate.typeParameters.find((candidate) => { const inferred = inferredArguments.get(Type.key(candidate.type)) return ( inferred !== undefined && @@ -2656,7 +2887,7 @@ export const analyzeStructLiteral = ( } else { const siteSubstitution = new Map() TypeInference.infer(expectedType, expression.type, siteSubstitution) - for (const parameter of struct.typeParameters) { + for (const parameter of aggregate.typeParameters) { const parameterKey = Type.key(parameter.type) const inferred = siteSubstitution.get(parameterKey) if ( @@ -2728,18 +2959,18 @@ export const analyzeStructLiteral = ( ) const completedArguments = - struct === undefined || nominal === undefined + aggregate === undefined || nominal === undefined ? undefined : nominal.arguments.map((argument, ordinal): Type.GenericArgument => { - const parameter = struct.typeParameters.at(ordinal)?.type + const parameter = aggregate.typeParameters.at(ordinal)?.type if (parameter === undefined) return argument return inferredArguments.get(Type.key(parameter))?.argument ?? argument }) let unresolvedParameters: DeclarationFacts.TypeParameterFact[] - if (struct === undefined || completedArguments === undefined) { + if (aggregate === undefined || completedArguments === undefined) { unresolvedParameters = [] } else { - unresolvedParameters = struct.typeParameters.flatMap((parameter, ordinal) => { + unresolvedParameters = aggregate.typeParameters.flatMap((parameter, ordinal) => { const argument = completedArguments.at(ordinal) return argument !== undefined && isOwnStructArgument(parameter.type, argument) ? [parameter] @@ -2755,15 +2986,15 @@ export const analyzeStructLiteral = ( nominal === undefined || completedArguments === undefined || unresolvedParameters.length > 0 || - (struct !== undefined && + (aggregate !== undefined && TypeInference.substitution( - struct.typeParameters.map((parameter) => parameter.type), + aggregate.typeParameters.map((parameter) => parameter.type), completedArguments, ) === undefined) ? undefined : Type.nominal(nominal.module, nominal.name, completedArguments) const typeArguments: ReadonlyArray = Object.freeze( - struct?.typeParameters.map((parameter, ordinal) => { + aggregate?.typeParameters.map((parameter, ordinal) => { const parameterKey = Type.key(parameter.type) const argument = completedArguments?.at(ordinal) const origins = argumentOrigins.get(parameterKey) ?? Object.freeze([]) @@ -2780,13 +3011,8 @@ export const analyzeStructLiteral = ( }) }) ?? [], ) - const completedTarget: StructTargetFact = - completedNominal !== undefined && target.fact._tag === 'Resolved' - ? Object.freeze({ ...target.fact, type: completedNominal }) - : target.fact - - if (struct !== undefined && completedNominal !== undefined) { - for (const field of struct.fields) { + if (aggregate !== undefined && completedNominal !== undefined) { + for (const field of aggregateFields) { if (field.name._tag !== 'Present' || seen.has(field.name.spelling)) continue if (field.visibility === 'Private' && completedNominal.module !== source.id) continue diagnostics.push( @@ -2800,10 +3026,10 @@ export const analyzeStructLiteral = ( } let fields: { field: DeclarationFacts.FieldFact; initializer: StructInitializerFact }[] - if (struct === undefined) { + if (aggregate === undefined) { fields = [] } else { - fields = struct.fields.flatMap((field) => { + fields = aggregateFields.flatMap((field) => { if (field.name._tag !== 'Present') return [] const fieldName = field.name.spelling const initializer = initializers.find( @@ -2813,28 +3039,61 @@ export const analyzeStructLiteral = ( }) } const complete = - struct !== undefined && + aggregate !== undefined && completedNominal !== undefined && authorized && SyntaxTree.isAvailableSyntax(node) && - fields.length === struct.fields.length && - initializers.length === struct.fields.length && + fields.length === aggregateFields.length && + initializers.length === aggregateFields.length && initializers.every((initializer) => initializer.state._tag === 'Resolved') const type = complete && completedNominal !== undefined ? availableExpressionType(completedNominal) : unavailableExpressionType - return Object.freeze({ - fact: Object.freeze({ + let fact: ExpressionFact + if (unionTarget === undefined) { + let structTarget: StructTargetFact + if (target.fact._tag === 'Resolved' && 'struct' in target.fact) { + structTarget = + completedNominal === undefined + ? target.fact + : Object.freeze({ ...target.fact, type: completedNominal }) + } else { + structTarget = Object.freeze({ + _tag: 'Unavailable', + ...(target.fact._tag === 'Unavailable' && target.fact.cause !== undefined + ? { cause: target.fact.cause } + : {}), + }) + } + fact = Object.freeze({ _tag: 'StructLiteral', - target: completedTarget, + target: structTarget, authorized, typeArguments, initializers: Object.freeze(initializers), fields: Object.freeze(fields), type, syntax: node, - }), + }) + } else { + const variantTarget: UnionVariantTargetFact = + completedNominal !== undefined && unionTarget.fact._tag === 'Resolved' + ? Object.freeze({ ...unionTarget.fact, type: completedNominal }) + : unionTarget.fact + fact = Object.freeze({ + _tag: 'UnionVariant', + target: variantTarget, + authorized, + typeArguments, + initializers: Object.freeze(initializers), + fields: Object.freeze(fields), + type, + syntax: node, + }) + } + return Object.freeze({ + fact, diagnostics: Object.freeze(diagnostics), type: complete ? completedNominal : undefined, }) @@ -4792,6 +5051,7 @@ export const effectCaptureFacts = ( expression(fact.index) return case 'StructLiteral': + case 'UnionVariant': for (const item of fact.initializers) expression(item.expression) return case 'ArrayLiteral': @@ -5407,8 +5667,8 @@ export function analyzeExpression( ) } - if (node.kind === 'StructLiteralExpression') { - return analyzeStructLiteral(source, node, declarations, declaration, scope, resolution) + if (node.kind === 'StructLiteralExpression' || node.kind === 'UnionVariantExpression') { + return analyzeAggregateLiteral(source, node, declarations, declaration, scope, resolution) } if (node.kind === 'ArrayLiteralExpression') { @@ -5416,6 +5676,8 @@ export function analyzeExpression( } if (node.kind === 'FieldProjectionExpression') { + if (resolveBareUnionVariantTarget(source, node, resolution) !== undefined) + return analyzeAggregateLiteral(source, node, declarations, declaration, scope, resolution) return ( analyzeEnumMember(source, node, resolution, expected) ?? analyzeConstantReference(source, node, resolution) ?? diff --git a/packages/compiler/src/HirLowering.ts b/packages/compiler/src/HirLowering.ts index ed55e8c6d..0c9da9271 100644 --- a/packages/compiler/src/HirLowering.ts +++ b/packages/compiler/src/HirLowering.ts @@ -747,6 +747,9 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. span: fact.syntax.span, }) } + if (fact._tag === 'UnionVariant') { + return Object.freeze({ _tag: 'Unavailable', span: fact.syntax.span }) + } if (fact._tag === 'ArrayLiteral') { if (fact.state._tag !== 'Complete' || fact.type._tag !== 'Available') { return Object.freeze({ _tag: 'Unavailable', span: fact.syntax.span }) @@ -1588,6 +1591,7 @@ export const directExpressionChildren = ( case 'ArrayLiteral': return Object.freeze(expression.elements.map((element) => element.expression)) case 'StructLiteral': + case 'UnionVariant': return Object.freeze(expression.initializers.map((initializer) => initializer.expression)) case 'Grouped': return Object.freeze([expression.expression]) diff --git a/packages/compiler/src/Ownership.ts b/packages/compiler/src/Ownership.ts index a49a7cff5..46776a2ea 100644 --- a/packages/compiler/src/Ownership.ts +++ b/packages/compiler/src/Ownership.ts @@ -1389,7 +1389,7 @@ const analyzeLoans = ( ? Object.freeze([site.binding.ordinal]) : movedExecutableBindings(expression.subject) } - if (expression._tag === 'StructLiteral') + if (expression._tag === 'StructLiteral' || expression._tag === 'UnionVariant') return Object.freeze( expression.initializers.flatMap((initializer) => movedExecutableBindings(initializer.expression), @@ -1473,6 +1473,7 @@ const analyzeLoans = ( scanRunEnds(expression.index, region) return case 'StructLiteral': + case 'UnionVariant': for (const initializer of expression.initializers) scanRunEnds(initializer.expression, region) return @@ -1882,6 +1883,7 @@ const analyzeLoans = ( // enclosing binding carries reaches the captures stored inside it too. Without this, a // borrow captured by a stored callable would be released while the aggregate still holds it. case 'StructLiteral': + case 'UnionVariant': for (const initializer of expression.initializers) { inspect( initializer.expression, diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index a81fce33f..514071a77 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -984,6 +984,37 @@ const collectExpression = ( } return } + case 'UnionVariant': { + const token = expression.target._tag === 'Resolved' ? expression.target.token : undefined + if (expression.target._tag === 'Resolved') + push( + pending, + token?.span, + 'Value', + expression.target.variant.canonical._tag === 'Canonical' + ? available( + Object.freeze({ + _tag: 'UnionVariantIdentity', + id: expression.target.variant.canonical.id, + }), + ) + : Object.freeze({ _tag: 'Unavailable' }), + locationOfUnionVariant(expression.target.variant), + ) + for (const initializer of expression.initializers) { + const fieldToken = initializer.token + if (initializer.state._tag === 'Resolved' || initializer.state._tag === 'Inaccessible') + push( + pending, + fieldToken?.span, + 'Field', + available(Object.freeze({ _tag: 'FieldIdentity', id: initializer.state.field.id })), + locationOfField(index, initializer.state.field), + ) + collectExpression(initializer.expression, index, scope, pending) + } + return + } case 'Move': case 'Borrow': case 'Run': diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index e8e8dcbed..c68959649 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'b30e71f5b07c99ea41f34988e217d56f86ec98e04137a04e9a9024c2dd700a28' +export const compilerDigest = '6a01d68b75898f872f3c39154236c701f7b0dd4502874032b207076f7f95c631' diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 64f96718b..4e6fb9067 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -72,6 +72,108 @@ it.effect( }), ) +it.effect('elaborates nominal union variants as precise parent values', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSource( + 'union-values/construction', + ascii(`union Option { Some { value: T }, None } +union Result { Success { value: A }, Failure { error: E } } +union State { Ready, Waiting { count: i32 } } +fn some() -> Option { return Option.Some { value: 42 } } +fn none() -> Option { return Option.None } +fn failed() -> Result { return Result.Failure { error: true } } +fn ready() -> State { return State.Ready }`), + ) + const functions = Analysis.rootAnalysis(self).functions + const some = functions.at(0)?.returnedExpression + const none = functions.at(1)?.returnedExpression + const failed = functions.at(2)?.returnedExpression + const ready = functions.at(3)?.returnedExpression + + for (const [name, expression] of [ + ['some', some], + ['none', none], + ['failed', failed], + ['ready', ready], + ] as const) { + assert.strictEqual(expression?._tag, 'UnionVariant', name) + if (expression?._tag === 'UnionVariant') assert.strictEqual(expression.type._tag, 'Available') + } + assert.strictEqual( + some?.type._tag === 'Available' ? Type.encode(some.type.type) : undefined, + 'union-values/construction.Option', + ) + assert.strictEqual( + none?.type._tag === 'Available' ? Type.encode(none.type.type) : undefined, + 'union-values/construction.Option', + ) + assert.strictEqual( + failed?.type._tag === 'Available' ? Type.encode(failed.type.type) : undefined, + 'union-values/construction.Result', + ) + assert.strictEqual( + ready?.type._tag === 'Available' ? Type.encode(ready.type.type) : undefined, + 'union-values/construction.State', + ) + assert.deepEqual(Analysis.diagnostics(self), []) + }), +) + +it.effect('infers union arguments only from the selected variant fields', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSource( + 'union-values/inference', + ascii(`union Option { Some { value: T }, None } +union Result { Success { value: A }, Failure { error: E } } +fn missingError() -> Result { return Result.Success { value: 42 } } +fn missingItem() -> Option { return Option.None } +fn unknown() -> Option { return Option.Missing } +fn conflict() -> Result { + return Result.Success { value: true } +}`), + ) + + assert.deepEqual( + Analysis.diagnostics(self).map((diagnostic) => diagnostic.code), + ['SEM0099', 'SEM0099', 'SEM0099', 'SEM0167', 'SEM0100'], + ) + const functions = Analysis.rootAnalysis(self).functions + assert.strictEqual(functions.at(0)?.returnedExpression._tag, 'UnionVariant') + assert.strictEqual(functions.at(1)?.returnedExpression._tag, 'UnionVariant') + assert.strictEqual(functions.at(2)?.returnedExpression._tag, 'UnionVariant') + assert.ok( + functions.every((fn) => fn.returnedExpression.type._tag === 'Unavailable'), + 'expected-type context must not complete a constructor application', + ) + }), +) + +it.effect('uses union variant field visibility as the external construction boundary', () => + Effect.gen(function* () { + const self = yield* multiSnapshot( + 'app/Main', + new Map([ + [ + 'model/Secret', + ascii(`pub union Secret { Open { pub value: i32, key: i32 }, Closed } +pub fn make(value: i32) -> Secret { return Secret.Open { value: value, key: 7 } }`), + ], + [ + 'app/Main', + ascii(`import model.Secret { Secret } +pub fn main() -> i32 { let secret = Secret.Open { value: 1, key: 2 } return 0 }`), + ], + ]), + ) + + assert.deepEqual( + Analysis.diagnostics(self).map((diagnostic) => diagnostic.code), + ['SEM0021'], + ) + assert.notInclude(Analysis.diagnostics(self).at(0)?.message ?? '', 'key') + }), +) + it.effect('evaluates initializers in source order before constructing in declaration order', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( From fa4a734cc8b8daadae4c85b92ebcb7492d1ca46a Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 13:44:50 -0300 Subject: [PATCH 06/42] feat(compiler): plan nominal union layouts --- openspec/changes/add-nominal-unions/tasks.md | 14 +- packages/compiler/src/Hir.ts | 27 +- packages/compiler/src/HirLowering.ts | 47 +++- .../compiler/src/InspectorProjectBackend.ts | 3 + packages/compiler/src/Layout.ts | 265 +++++++++++++++++- packages/compiler/src/LayoutEncode.ts | 17 ++ packages/compiler/src/LayoutVerify.ts | 169 +++++++++++ .../src/ToolchainIntegrity.generated.ts | 2 +- .../compiler/src/internal/CallingShape.ts | 17 ++ packages/compiler/test/Layout.test.ts | 90 +++++- packages/compiler/test/StructValues.test.ts | 40 +++ 11 files changed, 677 insertions(+), 14 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index ea1185ae5..139d4ae7f 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -21,8 +21,8 @@ - [x] 3.2 Refactor struct-literal field checking into a shared aggregate-field elaborator without changing existing struct facts or diagnostics, and verify the existing struct construction and generic-inference suites remain byte-for-byte stable where golden data exists. - [x] 3.3 Implement two-stage variant constructor resolution—parent declaration and explicit prefix first, field-only suffix inference second—and verify zero-prefix, partial-prefix, conflicting, and parent-only uninferred argument cases. - [x] 3.4 Implement unit and named-field construction with complete field initialization, construction authority, visibility fences, type compatibility, represented fields, and precise parent result types, and verify cross-module private fields block raw construction. -- [ ] 3.5 Preserve every variant through generic specialization, including equal and `never` payloads while independently renormalizing structural-union fields, and verify specialization facts never collapse or flatten variants. -- [ ] 3.6 Reject direct parent-union field projection and common-field synthesis while retaining diagnostic facts, and verify `result.value` is unavailable until a variant pattern binds its payload. +- [x] 3.5 Preserve every variant through generic specialization, including equal and `never` payloads while independently renormalizing structural-union fields, and verify specialization facts never collapse or flatten variants. +- [x] 3.6 Reject direct parent-union field projection and common-field synthesis while retaining diagnostic facts, and verify `result.value` is unavailable until a variant pattern binds its payload. - [ ] 3.7 Admit interface, operator, Copy, and Drop declarations against nominal union parents through the ordinary conformance/coherence path, and verify variant names do not become lookup or implementation targets. ## 4. Variant Patterns and Hierarchical Coverage @@ -44,11 +44,11 @@ ## 6. Target Layout and Calling Shapes -- [ ] 6.1 Extend the inline dependency graph and nominal layout catalog to include complete non-generic unions and mixed struct/union cycles, and verify unused private and unavailable union entries appear before runtime reachability. -- [ ] 6.2 Add a distinct nominal-union representation plan with deterministic private tags, source-order ordinals, payload offset, maximum size/alignment, total padding, and per-variant aggregate layouts, and verify unit, padded multi-field, and `never` payload cases. -- [ ] 6.3 Specialize reachable generic union layouts without speculative open-generic entries, and verify equivalent concrete applications reuse one catalog identity while distinct applications receive distinct physical plans. -- [ ] 6.4 Publish a backend-neutral tag-plus-payload calling shape with complete per-variant logical-field mappings, and verify call/return plans for heterogeneous variants are deterministic and unavailable dependencies stop before MIR. -- [ ] 6.5 Extend layout encoding, verification, and Analysis projections with nominal-union facts under unambiguous internal names, and verify no nominal tag, padding, or ABI detail becomes source-observable. +- [x] 6.1 Extend the inline dependency graph and nominal layout catalog to include complete non-generic unions and mixed struct/union cycles, and verify unused private and unavailable union entries appear before runtime reachability. +- [x] 6.2 Add a distinct nominal-union representation plan with deterministic private tags, source-order ordinals, payload offset, maximum size/alignment, total padding, and per-variant aggregate layouts, and verify unit, padded multi-field, and `never` payload cases. +- [x] 6.3 Specialize reachable generic union layouts without speculative open-generic entries, and verify equivalent concrete applications reuse one catalog identity while distinct applications receive distinct physical plans. +- [x] 6.4 Publish a backend-neutral tag-plus-payload calling shape with complete per-variant logical-field mappings, and verify call/return plans for heterogeneous variants are deterministic and unavailable dependencies stop before MIR. +- [x] 6.5 Extend layout encoding, verification, and Analysis projections with nominal-union facts under unambiguous internal names, and verify no nominal tag, padding, or ABI detail becomes source-observable. ## 7. HIR, MIR, and Verification diff --git a/packages/compiler/src/Hir.ts b/packages/compiler/src/Hir.ts index 3b0e789f0..14ceab156 100644 --- a/packages/compiler/src/Hir.ts +++ b/packages/compiler/src/Hir.ts @@ -550,6 +550,20 @@ export type Expression = readonly type: DeclarationFacts.SemanticType readonly span: SourceSpan.SourceSpan } + | { + readonly _tag: 'ConstructUnionVariant' + readonly nominal: Type.Nominal + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly variantOrdinal: number + /** Field identities in language evaluation order; `fields` remains variant storage order. */ + readonly evaluationOrder: ReadonlyArray + readonly fields: ReadonlyArray<{ + readonly field: DeclarationFacts.FieldId + readonly value: Expression + }> + readonly type: DeclarationFacts.SemanticType + readonly span: SourceSpan.SourceSpan + } | { readonly _tag: 'ArrayConstruct' readonly elements: ReadonlyArray @@ -1010,6 +1024,7 @@ export const expressionChildren = (expression: Expression): ReadonlyArray field.value) case 'ArrayConstruct': return expression.elements @@ -1085,7 +1100,8 @@ export const firstUnavailable = ( return walk(expression.slice) case 'SliceIndexPlace': return walk(expression.slice) ?? walk(expression.index) - case 'Construct': { + case 'Construct': + case 'ConstructUnionVariant': { for (const field of expression.fields) { const found = walk(field.value) if (found !== undefined) return found @@ -1666,6 +1682,15 @@ const encodeExpression = (expression: Expression, depth: number): string => { `${indent} field #${field.ordinal}\n${encodeExpression(value, depth + 2)}`, ), ].join('\n') + case 'ConstructUnionVariant': + return [ + `${indent}construct-variant ${Type.encode(expression.nominal)}.${expression.variant.name}#${expression.variantOrdinal} : ${Type.encode(expression.type)} ${spanText(expression.span)}`, + `${indent} evaluation-order ${expression.evaluationOrder.map((field) => `#${field.ordinal}`).join(', ') || 'empty'}`, + ...expression.fields.map( + ({ field, value }) => + `${indent} field #${field.ordinal}\n${encodeExpression(value, depth + 2)}`, + ), + ].join('\n') case 'ArrayConstruct': return [ `${indent}construct-array ${Type.encode(expression.type)} elements=${expression.elements.length} ${spanText(expression.span)}`, diff --git a/packages/compiler/src/HirLowering.ts b/packages/compiler/src/HirLowering.ts index 0c9da9271..20ec54985 100644 --- a/packages/compiler/src/HirLowering.ts +++ b/packages/compiler/src/HirLowering.ts @@ -748,7 +748,52 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. }) } if (fact._tag === 'UnionVariant') { - return Object.freeze({ _tag: 'Unavailable', span: fact.syntax.span }) + if ( + fact.target._tag !== 'Resolved' || + fact.target.variant.canonical._tag !== 'Canonical' || + fact.type._tag !== 'Available' || + fact.fields.length !== fact.target.variant.fields.length + ) { + return Object.freeze({ + _tag: 'Unavailable', + span: fact.syntax.span, + ...(fact.target._tag === 'Unavailable' && fact.target.cause !== undefined + ? { cause: fact.target.cause } + : {}), + }) + } + const substitution = + TypeInference.substitution( + fact.target.union.typeParameters.map((parameter) => parameter.type), + fact.target.type.arguments, + ) ?? new Map() + return Object.freeze({ + _tag: 'ConstructUnionVariant', + nominal: fact.target.type, + variant: fact.target.variant.canonical.id, + variantOrdinal: fact.target.variant.id.ordinal, + evaluationOrder: Object.freeze( + fact.initializers.flatMap((initializer) => + initializer.state._tag === 'Resolved' ? [initializer.state.field.id] : [], + ), + ), + fields: Object.freeze( + fact.fields.map(({ field, initializer }) => { + const value = + field.declaredType._tag === 'Resolved' + ? hirExpectedExpression( + initializer.expression, + Type.substitute(field.declaredType.type, substitution), + 'StructField', + field.syntax.span, + ) + : hirExpression(initializer.expression) + return Object.freeze({ field: field.id, value }) + }), + ), + type: fact.type.type, + span: fact.syntax.span, + }) } if (fact._tag === 'ArrayLiteral') { if (fact.state._tag !== 'Complete' || fact.type._tag !== 'Available') { diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index 8df31bf05..011907e4b 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -665,6 +665,9 @@ export const layoutRows = ( case 'Union': representationText = `sum · tag i${entry.representation.tag.bits} · payload +${entry.representation.payloadOffset}/${entry.representation.payloadSize}` break + case 'NominalUnion': + representationText = `nominal union · ${entry.representation.variants.length} variants · tag i${entry.representation.tag.bits} · payload +${entry.representation.payloadOffset}/${entry.representation.payloadSize}` + break case 'Reference': representationText = `reference · address i${entry.representation.address.bits}` break diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 4da4c2267..16297e70e 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -149,6 +149,24 @@ export type Representation = readonly tagPadding: number readonly tailPadding: number } + | { + readonly _tag: 'NominalUnion' + readonly union: DeclarationFacts.CanonicalId + readonly tag: { readonly bits: 32; readonly size: 4 } + readonly variants: ReadonlyArray<{ + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly ordinal: number + readonly fields: ReadonlyArray + readonly size: number + readonly alignment: number + readonly tailPadding: number + }> + readonly payloadOffset: number + readonly payloadSize: number + readonly payloadAlignment: number + readonly tagPadding: number + readonly tailPadding: number + } /** One compiler-owned concrete layout entry. */ export interface Entry { @@ -618,7 +636,7 @@ export const neverEntry = (): Entry => }) const nominalOf = ( - declaration: DeclarationFacts.StructFact | DeclarationFacts.EnumFact, + declaration: DeclarationFacts.StructFact | DeclarationFacts.UnionFact | DeclarationFacts.EnumFact, ): Type.Nominal | undefined => declaration.canonical._tag === 'Canonical' ? Type.nominal(declaration.canonical.id.module, declaration.canonical.id.name) @@ -665,11 +683,15 @@ export const scalarEnumEntry = ( } const dependenciesOf = ( - struct: DeclarationFacts.StructFact, + aggregate: DeclarationFacts.StructFact | DeclarationFacts.UnionFact, substitution: Type.Substitution = new Map(), ): ReadonlyArray => { const dependencies = new Map() - for (const field of struct.fields) { + const fields = + aggregate._tag === 'StructDeclaration' + ? aggregate.fields + : aggregate.variants.flatMap((variant) => variant.fields) + for (const field of fields) { let types: ReadonlyArray = [] if (field.declaredType._tag === 'Resolved') { types = Type.nominals(Type.substitute(field.declaredType.type, substitution)) @@ -719,12 +741,25 @@ export const catalog = ( return type === undefined ? [] : [Object.freeze({ enum_, type })] }) .sort((left, right) => Type.compare(left.type, right.type)) + const unionDeclarations = index.modules + .flatMap((module) => module.unions) + .flatMap((union) => { + const type = nominalOf(union) + return type === undefined ? [] : [Object.freeze({ union, type })] + }) + .sort((left, right) => Type.compare(left.type, right.type)) const byType = new Map( declarations.map((declaration) => [ `${declaration.type.module}\u0000${declaration.type.name}`, declaration, ]), ) + const unionByType = new Map( + unionDeclarations.map((declaration) => [ + `${declaration.type.module}\u0000${declaration.type.name}`, + declaration, + ]), + ) const completed = new Map() for (const declaration of enumDeclarations) { const entry = scalarEnumEntry(target, declaration.enum_) @@ -1094,6 +1129,162 @@ export const catalog = ( completed.set(key, entry) return entry } + const unionDeclaration = unionByType.get(`${type.module}\u0000${type.name}`) + if (unionDeclaration !== undefined) { + const union = unionDeclaration.union + if (union.canonical._tag !== 'Canonical') { + const result = unavailable(type, Object.freeze([]), { + _tag: 'InvalidDeclaration', + detail: `canonical identity is unavailable for ${Type.encode(type)}`, + }) + completed.set(key, result) + return result + } + const parameters = union.typeParameters.map((parameter) => parameter.type) + const substitution = TypeInference.substitution(parameters, type.arguments) + const dependencies = + substitution === undefined ? Object.freeze([]) : dependenciesOf(union, substitution) + if (substitution === undefined) { + return unavailable(type, dependencies, { + _tag: 'InvalidDeclaration', + detail: `${Type.encode(type)} has ${type.arguments.length} type arguments; expected ${parameters.length}`, + }) + } + if (visiting.has(key)) { + const result = unavailable(type, dependencies, { + _tag: 'InvalidDeclaration', + detail: `recursive dependency for ${Type.encode(type)} was not rejected during declaration analysis`, + }) + completed.set(key, result) + return result + } + if (union.validity._tag !== 'Valid' || union.dependency._tag === 'Unavailable') { + let cause: Diagnostic.Identity | undefined + if (union.validity._tag === 'Invalid') cause = union.validity.causes.at(0) + else if (union.dependency._tag === 'Unavailable') cause = union.dependency.cause + const result = unavailable( + type, + dependencies, + { _tag: 'InvalidDeclaration', detail: `declaration dependencies are unavailable` }, + cause, + ) + completed.set(key, result) + return result + } + + visiting.add(key) + let fieldsCopy = true + let failure: UnavailableEntry | undefined + const variants: Array< + Extract['variants'][number] + > = [] + for (const variant of union.variants) { + if (variant.canonical._tag !== 'Canonical') { + failure = unavailable(type, dependencies, { + _tag: 'InvalidDeclaration', + detail: `variant identity is unavailable for ${Type.encode(type)}`, + }) + break + } + const inputs: Array>> = [] + for (const field of variant.fields) { + if ( + field.state._tag !== 'Unique' || + field.name._tag !== 'Present' || + field.declaredType._tag !== 'Resolved' || + field.declaredType.exposureCause !== undefined + ) { + let cause: Diagnostic.Identity | undefined + if (field.state._tag === 'Duplicate') cause = field.state.cause + else if (field.declaredType._tag === 'Unresolved') { + cause = field.declaredType.cause + } else if (field.declaredType._tag === 'Resolved') { + cause = field.declaredType.exposureCause + } + failure = unavailable( + type, + dependencies, + { _tag: 'UnavailableField', field: field.id, detail: 'field is unavailable' }, + cause, + ) + break + } + const fieldType = Type.substitute(field.declaredType.type, substitution) + const fieldLayout = layoutType(fieldType) + if (fieldLayout._tag === 'UnavailableLayoutEntry') { + failure = unavailable( + type, + dependencies, + { _tag: 'UnavailableDependency', dependency: fieldType }, + fieldLayout.cause, + ) + break + } + fieldsCopy = fieldsCopy && fieldLayout.copy + inputs.push( + Object.freeze({ + value: Object.freeze({ + _tag: 'LayoutField' as const, + id: field.id, + name: field.name.spelling, + type: fieldType, + }), + size: fieldLayout.size, + alignment: fieldLayout.alignment, + }), + ) + } + if (failure !== undefined) break + const packed = Packing.pack(inputs) + variants.push( + Object.freeze({ + variant: variant.canonical.id, + ordinal: variant.id.ordinal, + fields: Object.freeze( + packed.fields.map(({ value, ...placement }) => + Object.freeze({ ...value, ...placement }), + ), + ), + size: packed.size, + alignment: packed.alignment, + tailPadding: packed.tailPadding, + }), + ) + } + visiting.delete(key) + if (failure !== undefined) { + completed.set(key, failure) + return failure + } + const payloadAlignment = variants.reduce( + (maximum, variant) => Math.max(maximum, variant.alignment), + 1, + ) + const payloadSize = variants.reduce((maximum, variant) => Math.max(maximum, variant.size), 0) + const payloadOffset = alignUp(4, payloadAlignment) + const alignment = Math.max(4, payloadAlignment) + const size = alignUp(payloadOffset + payloadSize, alignment) + const entry: Entry = Object.freeze({ + _tag: 'LayoutEntry', + type, + copy: ConformanceProof.hasCopyDeclaration(index, type) && fieldsCopy, + size, + alignment, + representation: Object.freeze({ + _tag: 'NominalUnion', + union: union.canonical.id, + tag: Object.freeze({ bits: 32, size: 4 }), + variants: Object.freeze(variants), + payloadOffset, + payloadSize, + payloadAlignment, + tagPadding: payloadOffset - 4, + tailPadding: size - (payloadOffset + payloadSize), + }), + }) + completed.set(key, entry) + return entry + } const declaration = byType.get(`${type.module}\u0000${type.name}`) if (declaration === undefined) { return unavailable(type, Object.freeze([]), { @@ -1618,6 +1809,12 @@ export const catalog = ( for (const field of member.fields) { if (field.declaredType._tag === 'Resolved') addReferenced(field.declaredType.type) } + } else if (member._tag === 'UnionDeclaration') { + for (const variant of member.variants) { + for (const field of variant.fields) { + if (field.declaredType._tag === 'Resolved') addReferenced(field.declaredType.type) + } + } } else if (member._tag === 'ServiceDeclaration' || member._tag === 'InterfaceDeclaration') { for (const operation of member.operations) { for (const parameter of operation.parameters) @@ -1633,6 +1830,9 @@ export const catalog = ( for (const declaration of declarations) { if (declaration.struct.typeParameters.length === 0) layoutNominal(declaration.type) } + for (const declaration of unionDeclarations) { + if (declaration.union.typeParameters.length === 0) layoutNominal(declaration.type) + } for (const instance of discovery?.instances ?? []) { const substitution = instance.substitution if (instance.function.contract._tag === 'Contract') { @@ -3276,6 +3476,46 @@ const shapeNode = ( }) } const candidate = entries.get(Type.key(type)) + if (Type.isNominal(type) && candidate?.representation._tag === 'NominalUnion') { + const variants = Object.freeze( + candidate.representation.variants.map((variant) => { + const fields = Object.freeze( + variant.fields.map((field) => + Object.freeze({ field: field.id, shape: shapeNode(field.type, context) }), + ), + ) + const shape: CallingShapeNode = Object.freeze({ + _tag: 'ProductShape', + type, + fields, + laneCount: fields.reduce((total, field) => total + field.shape.laneCount, 0), + }) + return Object.freeze({ + variant: variant.variant, + ordinal: variant.ordinal, + shape, + payloadSlots: Object.freeze(Array.from({ length: shape.laneCount }, (_, slot) => slot)), + }) + }), + ) + const payloadLaneCount = variants.reduce( + (maximum, variant) => Math.max(maximum, variant.shape.laneCount), + 0, + ) + return Object.freeze({ + _tag: 'NominalUnionShape', + type, + tag: Object.freeze({ type: 'i32', lane: 0 }), + payloadLaneCount, + payloadTypes: unifyPayloadTypes( + variants.map((variant) => variant.shape), + target, + ), + zeroFill: true, + variants, + laneCount: 1 + payloadLaneCount, + }) + } if (Type.isFixedArray(type)) { const element = shapeNode(type.element, context) const laneCount = element.laneCount * type.length @@ -3480,6 +3720,25 @@ const materializeLanes = ( ), ]) } + if (node._tag === 'NominalUnionShape') { + return Object.freeze([ + Object.freeze({ + _tag: 'CallingLane' as const, + path: Object.freeze([...path, Object.freeze({ _tag: 'NominalUnionTagSelector' as const })]), + type: 'i32' as const, + }), + ...Array.from({ length: node.payloadLaneCount }, (_, slot) => + Object.freeze({ + _tag: 'CallingLane' as const, + path: Object.freeze([ + ...path, + Object.freeze({ _tag: 'NominalUnionPayloadSelector' as const, slot }), + ]), + type: node.payloadTypes.at(slot) ?? ('i32' as const), + }), + ), + ]) + } if (node._tag === 'OutcomeShape') { return Object.freeze([ Object.freeze({ diff --git a/packages/compiler/src/LayoutEncode.ts b/packages/compiler/src/LayoutEncode.ts index df70f390e..1844ad711 100644 --- a/packages/compiler/src/LayoutEncode.ts +++ b/packages/compiler/src/LayoutEncode.ts @@ -47,6 +47,8 @@ const representationText = (representation: Representation): string => { return `reference target=${Type.encode(representation.target)} address=i${representation.address.bits}@${representation.address.offset}/${representation.address.size}/${representation.address.alignment}` case 'Union': return `union tag=i${representation.tag.bits} payload-offset=${representation.payloadOffset} payload-size=${representation.payloadSize} payload-align=${representation.payloadAlignment} tag-padding=${representation.tagPadding} tail-padding=${representation.tailPadding}` + case 'NominalUnion': + return `nominal-union ${representation.union.module}.${representation.union.name} tag=i${representation.tag.bits} payload-offset=${representation.payloadOffset} payload-size=${representation.payloadSize} payload-align=${representation.payloadAlignment} tag-padding=${representation.tagPadding} tail-padding=${representation.tailPadding}` case 'Aggregate': { const cleanupHook = representation.cleanupHook === undefined @@ -123,6 +125,17 @@ const entryLines = (candidate: Entry): ReadonlyArray => { ` member ${member.ordinal} ${Type.encode(member.type)} size=${member.size} align=${member.alignment}`, ), ] + case 'NominalUnion': + return [ + header, + ...candidate.representation.variants.flatMap((variant) => [ + ` variant ${variant.ordinal} ${variant.variant.name} size=${variant.size} align=${variant.alignment} tail-padding=${variant.tailPadding}`, + ...variant.fields.map( + (field) => + ` field ${DeclarationFacts.fieldIdKey(field.id)} ${field.name}: ${Type.encode(field.type)} offset=${field.offset} size=${field.size} align=${field.alignment} padding=${field.padding}`, + ), + ]), + ] default: return [header] } @@ -174,6 +187,10 @@ export const encode = (self: Plan): string => return 'tag' case 'UnionPayloadSelector': return `payload[${selector.slot}]` + case 'NominalUnionTagSelector': + return 'nominal-tag' + case 'NominalUnionPayloadSelector': + return `nominal-payload[${selector.slot}]` case 'SliceAddressSelector': case 'ReferenceAddressSelector': return 'address' diff --git a/packages/compiler/src/LayoutVerify.ts b/packages/compiler/src/LayoutVerify.ts index d7c62eca5..390095b04 100644 --- a/packages/compiler/src/LayoutVerify.ts +++ b/packages/compiler/src/LayoutVerify.ts @@ -185,6 +185,46 @@ const representationEquals = (left: Representation, right: Representation): bool }) ) } + if (left._tag === 'NominalUnion') { + return ( + right._tag === 'NominalUnion' && + left.union.module === right.union.module && + left.union.name === right.union.name && + left.payloadOffset === right.payloadOffset && + left.payloadSize === right.payloadSize && + left.payloadAlignment === right.payloadAlignment && + left.tagPadding === right.tagPadding && + left.tailPadding === right.tailPadding && + left.variants.length === right.variants.length && + left.variants.every((variant, ordinal) => { + const other = right.variants.at(ordinal) + return ( + other !== undefined && + variant.variant.union.module === other.variant.union.module && + variant.variant.union.name === other.variant.union.name && + variant.variant.name === other.variant.name && + variant.ordinal === other.ordinal && + variant.size === other.size && + variant.alignment === other.alignment && + variant.tailPadding === other.tailPadding && + variant.fields.length === other.fields.length && + variant.fields.every((field, fieldOrdinal) => { + const otherField = other.fields.at(fieldOrdinal) + return ( + otherField !== undefined && + DeclarationFacts.sameFieldId(field.id, otherField.id) && + field.name === otherField.name && + Type.equals(field.type, otherField.type) && + field.offset === otherField.offset && + field.size === otherField.size && + field.alignment === otherField.alignment && + field.padding === otherField.padding + ) + }) + ) + }) + ) + } const cleanupHooksEqual = ( leftHook: Extract['cleanupHook'], rightHook: Extract['cleanupHook'], @@ -692,6 +732,106 @@ const verifyEntry = ( ), ]) } + if (candidate.representation._tag === 'NominalUnion') { + const representation = candidate.representation + const unionViolations: Array = [] + if (!Type.isNominal(candidate.type)) { + return Object.freeze([ + invalid( + 'InvalidAggregate', + candidate.type, + `${Type.encode(candidate.type)} uses nominal-union storage without a nominal type`, + ), + ]) + } + const nominal = candidate.type + if ( + representation.union.module !== nominal.module || + representation.union.name !== nominal.name || + representation.tag.bits !== 32 || + representation.tag.size !== 4 + ) { + unionViolations.push( + invalid( + 'InvalidAggregate', + candidate.type, + `${Type.encode(candidate.type)} has a foreign nominal-union identity or tag`, + ), + ) + } + for (const [ordinal, variant] of representation.variants.entries()) { + const expected = variant.fields.map((field) => { + const fieldLayout = Type.isBuiltin(field.type) + ? scalarEntry(target, field.type) + : available.get(Type.key(field.type)) + return Object.freeze({ + value: field, + size: fieldLayout?.size ?? 0, + alignment: fieldLayout?.alignment ?? 1, + available: fieldLayout !== undefined, + }) + }) + const packed = Packing.pack(expected) + const fieldsValid = variant.fields.every((field, fieldOrdinal) => { + const facts = expected.at(fieldOrdinal) + const placement = packed.fields.at(fieldOrdinal) + return ( + facts?.available === true && + placement !== undefined && + field.offset === placement.offset && + field.size === facts.size && + field.alignment === facts.alignment && + field.padding === placement.padding + ) + }) + if ( + variant.ordinal !== ordinal || + variant.variant.union.module !== representation.union.module || + variant.variant.union.name !== representation.union.name || + !fieldsValid || + variant.size !== packed.size || + variant.alignment !== packed.alignment || + variant.tailPadding !== packed.tailPadding + ) { + unionViolations.push( + invalid( + 'InvalidAggregate', + candidate.type, + `${Type.encode(candidate.type)} variant ${variant.variant.name} has non-canonical physical facts`, + ), + ) + } + } + const payloadAlignment = representation.variants.reduce( + (maximum, variant) => Math.max(maximum, variant.alignment), + 1, + ) + const payloadSize = representation.variants.reduce( + (maximum, variant) => Math.max(maximum, variant.size), + 0, + ) + const payloadOffset = alignUp(4, payloadAlignment) + const alignment = Math.max(4, payloadAlignment) + const size = alignUp(payloadOffset + payloadSize, alignment) + if ( + representation.payloadAlignment !== payloadAlignment || + representation.payloadSize !== payloadSize || + representation.payloadOffset !== payloadOffset || + representation.tagPadding !== payloadOffset - 4 || + representation.tailPadding !== size - (payloadOffset + payloadSize) || + candidate.alignment !== alignment || + candidate.size !== size + ) { + unionViolations.push( + invalid( + 'InvalidAggregate', + candidate.type, + `${Type.encode(candidate.type)} has non-canonical nominal-union size or alignment`, + ), + ) + } + return Object.freeze(unionViolations) + } if (candidate.representation._tag !== 'Aggregate') { return Object.freeze([ invalid( @@ -854,6 +994,10 @@ export const selectorEquals = (left: Selector, right: Selector): boolean => { return right._tag === 'UnionTagSelector' case 'UnionPayloadSelector': return right._tag === 'UnionPayloadSelector' && left.slot === right.slot + case 'NominalUnionTagSelector': + return right._tag === 'NominalUnionTagSelector' + case 'NominalUnionPayloadSelector': + return right._tag === 'NominalUnionPayloadSelector' && left.slot === right.slot case 'SliceAddressSelector': return right._tag === 'SliceAddressSelector' case 'SliceLengthSelector': @@ -958,6 +1102,31 @@ export const laneOffset = ( } return undefined } + if (selector._tag === 'NominalUnionTagSelector') { + return ordinal === path.length - 1 && candidate.representation._tag === 'NominalUnion' + ? offset + : undefined + } + if (selector._tag === 'NominalUnionPayloadSelector') { + if (ordinal !== path.length - 1 || candidate.representation._tag !== 'NominalUnion') { + return undefined + } + const shape = callingShape(self, current) + if (shape?.tree._tag !== 'NominalUnionShape') return undefined + let payloadOffset = 0 + for (let slot = 0; slot <= selector.slot; slot += 1) { + const type = shape.tree.payloadTypes.at(slot) + if (type === undefined) return undefined + const scalar = entry(self, type) + if (scalar === undefined) return undefined + payloadOffset = alignUp(payloadOffset, scalar.alignment) + if (slot === selector.slot) { + return offset + candidate.representation.payloadOffset + payloadOffset + } + payloadOffset += scalar.size + } + return undefined + } if (selector._tag === 'SliceAddressSelector') { return ordinal === path.length - 1 && candidate.representation._tag === 'Slice' ? offset + candidate.representation.address.offset diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index c68959649..953e12e31 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '6a01d68b75898f872f3c39154236c701f7b0dd4502874032b207076f7f95c631' +export const compilerDigest = 'd2ec710501fe86dfb27b30368c5f37c786df12c2432909d0424830a3366f28cb' diff --git a/packages/compiler/src/internal/CallingShape.ts b/packages/compiler/src/internal/CallingShape.ts index 1be205c65..d5616deb8 100644 --- a/packages/compiler/src/internal/CallingShape.ts +++ b/packages/compiler/src/internal/CallingShape.ts @@ -22,6 +22,8 @@ export type Selector = | { readonly _tag: 'EffectCaptureSelector'; readonly ordinal: number } | { readonly _tag: 'UnionTagSelector' } | { readonly _tag: 'UnionPayloadSelector'; readonly slot: number } + | { readonly _tag: 'NominalUnionTagSelector' } + | { readonly _tag: 'NominalUnionPayloadSelector'; readonly slot: number } | { readonly _tag: 'SliceAddressSelector' } | { readonly _tag: 'SliceLengthSelector' } | { readonly _tag: 'StringStorageSelector' } @@ -106,6 +108,21 @@ export type CallingShapeNode = }> readonly laneCount: number } + | { + readonly _tag: 'NominalUnionShape' + readonly type: Type.Nominal + readonly tag: { readonly type: 'i32'; readonly lane: 0 } + readonly payloadLaneCount: number + readonly payloadTypes: ReadonlyArray + readonly zeroFill: true + readonly variants: ReadonlyArray<{ + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly ordinal: number + readonly shape: CallingShapeNode + readonly payloadSlots: ReadonlyArray + }> + readonly laneCount: number + } | { readonly _tag: 'OutcomeShape' readonly type: Type.Effect diff --git a/packages/compiler/test/Layout.test.ts b/packages/compiler/test/Layout.test.ts index a8dae5079..15527632d 100644 --- a/packages/compiler/test/Layout.test.ts +++ b/packages/compiler/test/Layout.test.ts @@ -403,8 +403,10 @@ enum Good { Only } pub fn main() -> i32 { return 0 }`), ) const catalog = Analysis.layoutCatalogOf(snapshot) + const plan = Analysis.layoutOf(snapshot) assert.strictEqual(catalog._tag, 'Available') - if (catalog._tag !== 'Available') return + assert.strictEqual(plan._tag, 'Available') + if (catalog._tag !== 'Available' || plan._tag !== 'Available') return assert.strictEqual( Layout.catalogEntry(catalog.value, Type.nominal('layout/invalid-scalar-enum', 'Broken')) ?._tag, @@ -678,6 +680,92 @@ pub fn main() -> i32 { let outer = make() return outer.pair.left }`), }), ) +it.effect('plans nominal union tags and variant-local payload layouts', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'layout/nominal-union', + ascii(`union State { Ready, Data { flag: bool, value: i64 }, Empty { impossible: never } } +union Box { Full { value: T }, Vacant } +fn retain(value: Box) -> Box { return move value } +pub fn main() -> i32 { return 0 }`), + 'wasm32-unknown-unknown', + ) + const catalog = Analysis.layoutCatalogOf(snapshot) + const plan = Analysis.layoutOf(snapshot) + assert.strictEqual(catalog._tag, 'Available') + assert.strictEqual(plan._tag, 'Available') + if (catalog._tag !== 'Available' || plan._tag !== 'Available') return + + const state = Layout.catalogEntry(catalog.value, Type.nominal('layout/nominal-union', 'State')) + const box = Layout.catalogEntry( + catalog.value, + Type.nominal('layout/nominal-union', 'Box', ['i32']), + ) + assert.strictEqual(state?._tag, 'LayoutEntry') + assert.strictEqual(box?._tag, 'LayoutEntry') + if ( + state?._tag !== 'LayoutEntry' || + state.representation._tag !== 'NominalUnion' || + box?._tag !== 'LayoutEntry' || + box.representation._tag !== 'NominalUnion' + ) + return + + assert.deepEqual( + state.representation.variants.map((variant) => [ + variant.variant.name, + variant.ordinal, + variant.fields.map((field) => [field.name, field.offset]), + ]), + [ + ['Ready', 0, []], + [ + 'Data', + 1, + [ + ['flag', 0], + ['value', 8], + ], + ], + ['Empty', 2, [['impossible', 0]]], + ], + ) + assert.deepEqual( + [ + state.representation.payloadOffset, + state.representation.payloadSize, + state.size, + state.alignment, + ], + [8, 16, 24, 8], + ) + assert.deepEqual( + box.representation.variants.map((variant) => [variant.variant.name, variant.size]), + [ + ['Full', 4], + ['Vacant', 0], + ], + ) + const boxType = Type.nominal('layout/nominal-union', 'Box', ['i32']) + const boxShape = Layout.callingShapes( + Target.wasm32UnknownUnknown, + catalog.value.entries.flatMap((entry) => (entry._tag === 'LayoutEntry' ? [entry] : [])), + [boxType], + ).at(0) + assert.strictEqual(boxShape?.tree._tag, 'NominalUnionShape') + assert.deepEqual( + boxShape?.lanes.map((lane) => lane.path.at(0)?._tag), + ['NominalUnionTagSelector', 'NominalUnionPayloadSelector'], + ) + assert.include( + LayoutEncode.encodeCatalog(catalog.value), + 'repr=nominal-union layout/nominal-union.State', + ) + assert.deepEqual(LayoutVerify.verifyCatalog(catalog.value), []) + assert.deepEqual(LayoutVerify.verify(plan.value), []) + }), +) + it.effect( 'retains unavailable fields, cycles, and transitive dependencies without harming peers', () => diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 4e6fb9067..f62fc634e 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -115,6 +115,27 @@ fn ready() -> State { return State.Ready }`), ready?.type._tag === 'Available' ? Type.encode(ready.type.type) : undefined, 'union-values/construction.State', ) + const hir = Analysis.rootAnalysis(self).hir.functions.map(Hir.returned) + assert.deepEqual( + hir.map((expression) => expression._tag), + [ + 'ConstructUnionVariant', + 'ConstructUnionVariant', + 'ConstructUnionVariant', + 'ConstructUnionVariant', + ], + ) + const someHir = hir.at(0) + assert.strictEqual( + someHir?._tag === 'ConstructUnionVariant' ? someHir.variant.name : undefined, + 'Some', + ) + assert.deepEqual( + someHir?._tag === 'ConstructUnionVariant' + ? someHir.fields.map((field) => field.field.ordinal) + : undefined, + [0], + ) assert.deepEqual(Analysis.diagnostics(self), []) }), ) @@ -174,6 +195,25 @@ pub fn main() -> i32 { let secret = Secret.Open { value: 1, key: 2 } return 0 }` }), ) +it.effect('does not synthesize fields on the nominal union parent', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSource( + 'union-values/projection', + ascii(`union Result { Success { value: A }, Failure { value: E } } +fn project(result: Result) -> i32 { return result.value }`), + ) + + assert.deepEqual( + Analysis.diagnostics(self).map((diagnostic) => diagnostic.code), + ['SEM0027'], + ) + assert.strictEqual( + Analysis.rootAnalysis(self).functions.at(0)?.returnedExpression.type._tag, + 'Unavailable', + ) + }), +) + it.effect('evaluates initializers in source order before constructing in declaration order', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( From d12fffecb0be72503b18cb4d0380e8c81e16e13f Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 13:54:02 -0300 Subject: [PATCH 07/42] feat(compiler): lower nominal union construction --- packages/compiler/src/BootstrapEvaluation.ts | 27 ++++++++ packages/compiler/src/BootstrapValue.ts | 12 ++++ packages/compiler/src/ExecutableOrigin.ts | 2 +- .../compiler/src/InspectorProjectBackend.ts | 12 +++- .../compiler/src/InspectorProjectSyntax.ts | 4 +- packages/compiler/src/InstanceDiagnostics.ts | 14 ++++- packages/compiler/src/Layout.ts | 2 +- packages/compiler/src/LowerExpression.ts | 61 +++++++++++++++++++ packages/compiler/src/Mir.ts | 15 +++++ packages/compiler/src/MirEncoding.ts | 4 +- packages/compiler/src/MirLinearization.ts | 1 + packages/compiler/src/MirVerification.ts | 43 +++++++++++++ packages/compiler/src/NativeOperation.ts | 1 + packages/compiler/src/NativePlaceOperation.ts | 34 +++++++++++ packages/compiler/src/Ownership.ts | 11 ++-- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 25 ++++++++ packages/compiler/src/WasmMemory.ts | 1 + packages/compiler/test/StructValues.test.ts | 32 ++++++++++ 19 files changed, 291 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index 6fd688cf8..e93a633a4 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -56,6 +56,7 @@ import type { ExecutionValue, FloatValue, IntegerValue, + NominalUnionValue, SharedCoreValue, SliceValue, StaticViewValue, @@ -80,6 +81,7 @@ export type { ExecutionValue, FloatValue, IntegerValue, + NominalUnionValue, RawBufferValue, ReferenceValue, SharedCoreValue, @@ -1649,6 +1651,7 @@ function* executeFunction( values.add(value) switch (value._tag) { case 'AggregateValue': + case 'NominalUnionValue': for (const field of value.fields) visit(field.value) return case 'ArrayValue': @@ -3941,6 +3944,30 @@ function* executeFunction( ) break } + case 'ConstructUnionVariant': { + const value: NominalUnionValue = Object.freeze({ + _tag: 'NominalUnionValue', + type: operation.type.type, + variant: operation.variant, + variantOrdinal: operation.variantOrdinal, + fields: Object.freeze( + operation.fields.map((field) => + Object.freeze({ field: field.field, value: read(field.value).value }), + ), + ), + }) + write(operation.destination, { value, fromCall: false }) + trace.push( + Object.freeze({ + _tag: 'Construct', + function: fn.id, + type: value.type, + fieldCount: value.fields.length, + span: operation.provenance.span, + }), + ) + break + } case 'ConstructArray': { const array: ArrayValue = Object.freeze({ _tag: 'ArrayValue', diff --git a/packages/compiler/src/BootstrapValue.ts b/packages/compiler/src/BootstrapValue.ts index 44797a102..201a1cf35 100644 --- a/packages/compiler/src/BootstrapValue.ts +++ b/packages/compiler/src/BootstrapValue.ts @@ -47,6 +47,17 @@ export interface AggregateValue { }> } +export interface NominalUnionValue { + readonly _tag: 'NominalUnionValue' + readonly type: Type.Nominal + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly variantOrdinal: number + readonly fields: ReadonlyArray<{ + readonly field: DeclarationFacts.FieldId + readonly value: Value + }> +} + export interface ArrayValue { readonly _tag: 'ArrayValue' readonly type: Type.FixedArray @@ -248,6 +259,7 @@ export type Value = | CharacterValue | FloatValue | AggregateValue + | NominalUnionValue | ArrayValue | SliceValue | StaticViewValue diff --git a/packages/compiler/src/ExecutableOrigin.ts b/packages/compiler/src/ExecutableOrigin.ts index 40b8cbefe..b4a43b6bd 100644 --- a/packages/compiler/src/ExecutableOrigin.ts +++ b/packages/compiler/src/ExecutableOrigin.ts @@ -332,7 +332,7 @@ export const make = (operations: Operations) => { ...callTargets(expression.index, index, substitution), ] } - if (expression._tag === 'Construct') { + if (expression._tag === 'Construct' || expression._tag === 'ConstructUnionVariant') { return expression.fields.flatMap((field) => callTargets(field.value, index, substitution)) } if (expression._tag === 'ArrayConstruct') { diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index 011907e4b..c98a01e88 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -1,4 +1,4 @@ -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import type * as DeclarationIndex from './DeclarationIndex.js' /** * Module, name, ownership, lowering and backend phases as rows. @@ -861,6 +861,10 @@ const operationLabel = (operation: Mir.Operation): string => { return `${localText(operation.destination)} = construct ${typeText(operation.type.type)} { ${operation.fields .map(({ field, value }) => `#${field.ordinal}: ${localText(value)}`) .join(', ')} }` + case 'ConstructUnionVariant': + return `${localText(operation.destination)} = construct ${typeText(operation.type.type)}.${operation.variant.name}#${operation.variantOrdinal} { ${operation.fields + .map(({ field, value }) => `${DeclarationFacts.fieldIdKey(field)}: ${localText(value)}`) + .join(', ')} }` case 'ConstructArray': return `${localText(operation.destination)} = array [${operation.elements .map(localText) @@ -1182,6 +1186,8 @@ const valueText = (value: BootstrapEvaluation.Value): string => { return `${value.enum.module}.${value.enum.name}.${value.member.name} = ${value.discriminant}` case 'AggregateValue': return `${typeText(value.type)} { ${value.fields.map((entry) => valueText(entry.value)).join(', ')} }` + case 'NominalUnionValue': + return `${typeText(value.type)}.${value.variant.name} { ${value.fields.map((entry) => valueText(entry.value)).join(', ')} }` } } @@ -1684,6 +1690,10 @@ const selectorPathText = (path: ReadonlyArray): string => return 'tag' case 'UnionPayloadSelector': return `payload[${selector.slot}]` + case 'NominalUnionTagSelector': + return 'nominal-tag' + case 'NominalUnionPayloadSelector': + return `nominal-payload[${selector.slot}]` case 'SliceAddressSelector': return 'address' default: diff --git a/packages/compiler/src/InspectorProjectSyntax.ts b/packages/compiler/src/InspectorProjectSyntax.ts index f9ee0b438..4abd12189 100644 --- a/packages/compiler/src/InspectorProjectSyntax.ts +++ b/packages/compiler/src/InspectorProjectSyntax.ts @@ -233,6 +233,8 @@ const hirExpressionLabel = (expression: Hir.Expression): string => { return `match ${expression.access.toLowerCase()} · ${expression.members.map(Match.encodeIdentity).join(' | ')}` case 'Construct': return `construct ${hirTypeText(expression.nominal)}` + case 'ConstructUnionVariant': + return `construct ${hirTypeText(expression.nominal)}.${expression.variant.name}` case 'ArrayConstruct': return `array ${hirTypeText(expression.type)} · ${expression.elements.length} elements` case 'Project': @@ -328,7 +330,7 @@ export const hirRows = (hir: Hir.Module): ReadonlyArray => { expression(node.subject, depth + 1, `${path}.s`) expression(node.index, depth + 1, `${path}.i`) } - if (node._tag === 'Construct') { + if (node._tag === 'Construct' || node._tag === 'ConstructUnionVariant') { // Canonical storage order, one child per field — the reordering from source order is the // struct-values pane's story; here the construct is just a typed expression tree. node.fields.forEach(({ field, value }) => { diff --git a/packages/compiler/src/InstanceDiagnostics.ts b/packages/compiler/src/InstanceDiagnostics.ts index 6a19c6f7d..b6fd7d8c2 100644 --- a/packages/compiler/src/InstanceDiagnostics.ts +++ b/packages/compiler/src/InstanceDiagnostics.ts @@ -147,7 +147,12 @@ export const representedNominals = ( .flatMap(Hir.statementExpressions) .flatMap(Hir.expressionTree) for (const expression of expressions) { - if (expression._tag !== 'Construct' && expression._tag !== 'ArrayConstruct') continue + if ( + expression._tag !== 'Construct' && + expression._tag !== 'ConstructUnionVariant' && + expression._tag !== 'ArrayConstruct' + ) + continue collectNominals( index, Specialization.specializeType(instance.key, expression.type, [instance.substitution]), @@ -259,7 +264,12 @@ const storedExecutableViolations = ( .flatMap(Hir.statementExpressions) .flatMap(Hir.expressionTree) .flatMap((expression) => { - if (expression._tag !== 'Construct' && expression._tag !== 'ArrayConstruct') return [] + if ( + expression._tag !== 'Construct' && + expression._tag !== 'ConstructUnionVariant' && + expression._tag !== 'ArrayConstruct' + ) + return [] const aggregate = Specialization.specializeType(instance.key, expression.type, [ instance.substitution, ]) diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 16297e70e..9004a7464 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -1990,7 +1990,7 @@ const addExpressionTypes = ( ) { addExpressionTypes(types, expression.root.value, substitution) } - if (expression._tag === 'Construct') { + if (expression._tag === 'Construct' || expression._tag === 'ConstructUnionVariant') { for (const field of expression.fields) addExpressionTypes(types, field.value, substitution) } if (expression._tag === 'ArrayConstruct') { diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 04c5205e1..ea19fd9dc 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -1594,6 +1594,67 @@ export function lowerExpressionInner( ) return { result: destination } } + case 'ConstructUnionVariant': { + const type = fn.type(expression.type) + if (type?._tag !== 'Nominal') return undefined + const representation = Layout.entry(fn.layout, type.type)?.representation + if (representation?._tag !== 'NominalUnion') return undefined + const variant = representation.variants.find( + (candidate) => + candidate.ordinal === expression.variantOrdinal && + candidate.variant.union.module === expression.variant.union.module && + candidate.variant.union.name === expression.variant.union.name && + candidate.variant.name === expression.variant.name, + ) + if (variant === undefined) return undefined + const canonicalFields = new Map( + expression.fields.map( + (field) => [DeclarationFacts.fieldIdKey(field.field), field] as const, + ), + ) + const loweredFields = new Map() + for (const fieldId of expression.evaluationOrder) { + const field = canonicalFields.get(DeclarationFacts.fieldIdKey(fieldId)) + if (field === undefined) return undefined + const lowered = lowerExpression(fn, field.value) + if (lowered === undefined) return undefined + loweredFields.set(DeclarationFacts.fieldIdKey(field.field), lowered.result) + } + const fields = expression.fields.flatMap((field) => { + const value = loweredFields.get(DeclarationFacts.fieldIdKey(field.field)) + const declared = variant.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), + ) + const stored = + declared === undefined + ? undefined + : (storedCallableValueType(fn.layout, declared.type)?.storage ?? + storedEffectValueType(fn.layout, declared.type)?.storage) + return value === undefined + ? [] + : [ + Object.freeze({ + field: field.field, + value, + ...(stored === undefined ? {} : { stored }), + }), + ] + }) + if (fields.length !== expression.fields.length) return undefined + const destination = fn.alloc(type) + fn.emit( + Object.freeze({ + _tag: 'ConstructUnionVariant', + destination, + type, + variant: expression.variant, + variantOrdinal: expression.variantOrdinal, + fields: Object.freeze(fields), + provenance: authored(expression.span), + }), + ) + return { result: destination } + } case 'ArrayConstruct': { const type = fn.type(expression.type) if (type?._tag !== 'FixedArray') return undefined diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index c013d7317..9bad7de0a 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -1029,6 +1029,21 @@ export type Operation = }> readonly provenance: Provenance } + | { + readonly _tag: 'ConstructUnionVariant' + readonly destination: LocalId + readonly type: Extract + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly variantOrdinal: number + readonly fields: ReadonlyArray<{ + readonly field: DeclarationFacts.FieldId + readonly value: LocalId + readonly stored?: + | Extract['storage'] + | Extract['storage'] + }> + readonly provenance: Provenance + } | { readonly _tag: 'ConstructArray' readonly destination: LocalId diff --git a/packages/compiler/src/MirEncoding.ts b/packages/compiler/src/MirEncoding.ts index 7f28de378..75e4a6386 100644 --- a/packages/compiler/src/MirEncoding.ts +++ b/packages/compiler/src/MirEncoding.ts @@ -1,4 +1,4 @@ -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import * as ExecutionTransition from './ExecutionTransition.js' import * as LayoutEncode from './LayoutEncode.js' import * as Match from './Match.js' @@ -181,6 +181,8 @@ const operationText = (operation: Operation): string => { return `${localText(operation.destination)} = close-effect-entry ${targetText(operation.target)} effect=${localText(operation.effect)} runner=${targetText(operation.runner)} outcome=${localText(operation.outcome)} failures=${operation.failures.map((failure) => `${failure.tag}:${SilkType.encode(failure.type)}->${localText(failure.payload)}:${failure.cleanup._tag}`).join(',') || 'none'} : i32 ${provenanceText(operation.provenance)}` case 'Construct': return `${localText(operation.destination)} = construct ${typeText(operation.type)} { ${operation.fields.map(({ field, value, stored }) => `#${field.ordinal}: ${localText(value)}${stored === undefined ? '' : ` stored=${storedExecutableText(stored)}`}`).join(', ')} } ${provenanceText(operation.provenance)}` + case 'ConstructUnionVariant': + return `${localText(operation.destination)} = construct-variant ${typeText(operation.type)}.${operation.variant.name}#${operation.variantOrdinal} { ${operation.fields.map(({ field, value, stored }) => `${DeclarationFacts.fieldIdKey(field)}: ${localText(value)}${stored === undefined ? '' : ` stored=${storedExecutableText(stored)}`}`).join(', ')} } ${provenanceText(operation.provenance)}` case 'ConstructArray': return `${localText(operation.destination)} = construct-array ${typeText(operation.type)} [${operation.elements.map(localText).join(', ')}] ${provenanceText(operation.provenance)}` case 'Project': diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index dccff6c24..2184b0911 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -109,6 +109,7 @@ export const destinationOf = (operation: LinearOperation): Mir.LocalId | undefin case 'ReifyEffect': case 'CloseEffectEntry': case 'Construct': + case 'ConstructUnionVariant': case 'ConstructArray': case 'Project': case 'ReadPlace': diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 39ab98e94..60e84a6ad 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -810,6 +810,7 @@ export const operationLocals = (operation: Operation): ReadonlyArray => ...operation.failures.map((failure) => failure.payload), ] case 'Construct': + case 'ConstructUnionVariant': return [operation.destination, ...operation.fields.map((field) => field.value)] case 'ConstructArray': return [operation.destination, ...operation.elements] @@ -1614,6 +1615,7 @@ const operationTypes = (operation: Operation): ReadonlyArray => { case 'CloseEffectEntry': return [] case 'Construct': + case 'ConstructUnionVariant': return operation.fields.map((field) => field.value) case 'ConstructArray': return operation.elements @@ -4624,6 +4627,46 @@ export const verify = (self: Module): ReadonlyArray => { ) } } + if (operation._tag === 'ConstructUnionVariant') { + const layout = Layout.entry(self.layout, operation.type.type) + const representation = + layout?.representation._tag === 'NominalUnion' ? layout.representation : undefined + const expected = representation?.variants.find( + (variant) => + variant.ordinal === operation.variantOrdinal && + variant.variant.union.module === operation.variant.union.module && + variant.variant.union.name === operation.variant.union.name && + variant.variant.name === operation.variant.name, + ) + const valid = + representation !== undefined && + expected !== undefined && + representation.union.module === operation.type.type.module && + representation.union.name === operation.type.type.name && + expected.fields.length === operation.fields.length && + operation.fields.every((field, ordinal) => { + const declared = expected.fields.at(ordinal) + const valueType = fn.localTypes.at(field.value.ordinal) + return ( + declared !== undefined && + DeclarationFacts.sameFieldId(declared.id, field.field) && + valueType !== undefined && + field.stored === undefined && + SilkType.equals(semanticType(valueType), declared.type) + ) + }) + if (!valid) { + violations.push( + Object.freeze({ + _tag: 'Violation', + rule: 'InvalidAggregateOperation', + function: fn.id, + region: region.id, + detail: `construction of ${typeText(operation.type)}.${operation.variant.name} does not match its canonical variant layout`, + }), + ) + } + } if (operation._tag === 'ConstructArray') { const semantic = operation.type.type const destination = fn.localTypes.at(operation.destination.ordinal) diff --git a/packages/compiler/src/NativeOperation.ts b/packages/compiler/src/NativeOperation.ts index 160b6fbd8..8d68ee215 100644 --- a/packages/compiler/src/NativeOperation.ts +++ b/packages/compiler/src/NativeOperation.ts @@ -99,6 +99,7 @@ export const emit = Effect.fnUntraced(function* ( case 'SliceLength': case 'ConvertUnion': case 'Construct': + case 'ConstructUnionVariant': case 'ConstructArray': case 'Project': case 'ReadPlace': diff --git a/packages/compiler/src/NativePlaceOperation.ts b/packages/compiler/src/NativePlaceOperation.ts index 47695f8db..50b84da0b 100644 --- a/packages/compiler/src/NativePlaceOperation.ts +++ b/packages/compiler/src/NativePlaceOperation.ts @@ -27,6 +27,7 @@ type Operation = Extract< | 'SliceLength' | 'ConvertUnion' | 'Construct' + | 'ConstructUnionVariant' | 'ConstructArray' | 'Project' | 'ReadPlace' @@ -421,6 +422,39 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op ), ) break + case 'ConstructUnionVariant': { + const targetLanes = NativeType.lanesFor(types, operation.type) + const tagLane = targetLanes.at(0) + if (tagLane === undefined) throw new RangeError('LLVM nominal union lost its tag lane') + const tag = yield* Constant.integerSigned(builder, i32, BigInt(operation.variantOrdinal)) + const sourceValues = operation.fields.flatMap((field) => [ + ...NativeStorage.readLocal(nativeStorage, field.value), + ]) + const sourceLanes = operation.fields.flatMap((field) => { + const fieldType = entry.fn.localTypes.at(field.value.ordinal) + return fieldType === undefined ? [] : [...NativeType.lanesFor(types, fieldType)] + }) + const payload: Array = [] + for (let ordinal = 1; ordinal < targetLanes.length; ordinal += 1) { + const targetLane = targetLanes.at(ordinal) + if (targetLane === undefined) throw new RangeError('LLVM nominal union lost a payload lane') + const input = sourceValues.at(ordinal - 1) + const sourceLane = sourceLanes.at(ordinal - 1) + payload.push( + input === undefined || sourceLane === undefined + ? yield* Constant.nullValue(builder, NativeType.laneType(types, targetLane)) + : yield* NativeArith.coerceLane( + arith.lane, + input, + sourceLane, + targetLane, + `nominal_union${operation.destination.ordinal}_${ordinal - 1}`, + ), + ) + } + nativeStorage.locals.set(operation.destination.ordinal, Object.freeze([tag, ...payload])) + break + } case 'ConstructArray': nativeStorage.locals.set( operation.destination.ordinal, diff --git a/packages/compiler/src/Ownership.ts b/packages/compiler/src/Ownership.ts index 46776a2ea..e82d51a47 100644 --- a/packages/compiler/src/Ownership.ts +++ b/packages/compiler/src/Ownership.ts @@ -1,6 +1,6 @@ import * as CleanupPlan from './CleanupPlan.js' import * as ConformanceProof from './ConformanceProof.js' -import type * as DeclarationFacts from './DeclarationFacts.js' +import * as DeclarationFacts from './DeclarationFacts.js' import type * as DeclarationIndex from './DeclarationIndex.js' import * as Diagnostic from './Diagnostic.js' import * as Elaboration from './Elaboration.js' @@ -796,12 +796,15 @@ const checkExpression = ( checkExpression(state, live, expression.left, false, guard, escaping) checkExpression(state, live, expression.right, false, guard, escaping) return - case 'Construct': { + case 'Construct': + case 'ConstructUnionVariant': { const fields = new Map( - expression.fields.map((field) => [field.field.ordinal, field.value] as const), + expression.fields.map( + (field) => [DeclarationFacts.fieldIdKey(field.field), field.value] as const, + ), ) for (const field of expression.evaluationOrder) { - const value = fields.get(field.ordinal) + const value = fields.get(DeclarationFacts.fieldIdKey(field)) if (value !== undefined) checkExpression(state, live, value, true, guard, escaping) } return diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 953e12e31..c74dec1ea 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'd2ec710501fe86dfb27b30368c5f37c786df12c2432909d0424830a3366f28cb' +export const compilerDigest = '87e72cb9853c5f259ee26eaa3f0860733f06d998a8cbd4fa7f5695bac0682bba' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index c058fbdaa..0defe5915 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -4972,6 +4972,29 @@ const emitConstructOperation = ( ) } +const emitConstructUnionVariantOperation = ( + operation: Extract, + state: WasmOperationContext, +): ReadonlyArray => { + const { slots, copy, zeroFor } = state + const destination = slots(operation.destination) + const tag = destination.at(0) + if (tag === undefined) throw new RangeError('Wasm nominal union lost its tag lane') + const payload = operation.fields.flatMap((field) => [...slots(field.value)]) + const payloadTargets = destination.slice(1, payload.length + 1) + if (payloadTargets.length !== payload.length) { + throw new RangeError('Wasm nominal union payload exceeds its calling shape') + } + return [ + Instr.i32Const(operation.variantOrdinal), + Instr.localSet(tag), + ...copy(payload, payloadTargets), + ...destination + .slice(payload.length + 1) + .flatMap((target) => [zeroFor(target), Instr.localSet(target)]), + ] +} + const emitConstructArrayOperation = ( operation: Extract, state: WasmOperationContext, @@ -7110,6 +7133,8 @@ const emitOperationWithContext = ( return emitConvertUnionOperation(operation, context) case 'Construct': return emitConstructOperation(operation, context) + case 'ConstructUnionVariant': + return emitConstructUnionVariantOperation(operation, context) case 'ConstructArray': return emitConstructArrayOperation(operation, context) case 'Project': diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 7957b20fc..6232a4370 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -186,6 +186,7 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan changed = include(operation.root, [operation.source]) || changed break case 'Construct': + case 'ConstructUnionVariant': changed = include( operation.destination, diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index f62fc634e..5a979dbf3 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -214,6 +214,38 @@ fn project(result: Result) -> i32 { return result.value }`), }), ) +it.effect('lowers and evaluates nominal union construction as a whole value', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/runtime', + ascii(`union State { Ready, Waiting { count: i32 } } +fn make() -> State { return State.Waiting { count: 2 } } +fn keep(state: State) -> State { return move state } +pub fn main() -> i32 { let state = keep(make()) return 42 }`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + assert.strictEqual(Analysis.loweredMir(self)._tag, 'MirModule') + const make = Analysis.loweredMir(self).functions.find((fn) => fn.id.name === 'make') + assert.strictEqual( + make === undefined + ? undefined + : MirVerification.operations(make).find( + (operation) => operation._tag === 'ConstructUnionVariant', + )?._tag, + 'ConstructUnionVariant', + ) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(self)), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + it.effect('evaluates initializers in source order before constructing in declaration order', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( From df95ddab7bd1bf0d41fc7c54d552f29e02f8f5ca Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 14:09:07 -0300 Subject: [PATCH 08/42] feat(compiler): match nominal union variants --- packages/compiler/src/BootstrapEvaluation.ts | 32 +++- packages/compiler/src/BootstrapStorage.ts | 11 +- packages/compiler/src/Elaboration.ts | 13 ++ packages/compiler/src/ExpressionAnalysis.ts | 166 +++++++++++++++--- packages/compiler/src/HirLowering.ts | 4 + packages/compiler/src/Layout.ts | 64 +++++++ packages/compiler/src/LowerExpression.ts | 19 +- packages/compiler/src/LowerStatements.ts | 33 ++-- packages/compiler/src/Match.ts | 68 +++++-- packages/compiler/src/MirLinearization.ts | 20 ++- packages/compiler/src/MirVerification.ts | 43 ++++- packages/compiler/src/NativeControl.ts | 74 ++++++-- packages/compiler/src/NativeValueOperation.ts | 2 +- packages/compiler/src/SemanticOccurrence.ts | 37 ++++ packages/compiler/src/StatementAnalysis.ts | 14 +- packages/compiler/src/WasmBackend.ts | 43 ++++- packages/compiler/test/StructValues.test.ts | 42 +++++ 17 files changed, 583 insertions(+), 102 deletions(-) diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index e93a633a4..5a4b7d38c 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -1939,10 +1939,27 @@ function* executeFunction( } case 'Match': { const scrutinee = read(operation.scrutinee).value - const activeIdentity = - scrutinee._tag === 'EnumValue' - ? Match.enumMember(scrutinee.enum, scrutinee.member) - : undefined + let activeIdentity: Match.CoverageIdentity | undefined + if (scrutinee._tag === 'EnumValue') { + activeIdentity = Match.enumMember(scrutinee.enum, scrutinee.member) + } else if (scrutinee._tag === 'NominalUnionValue') { + activeIdentity = Match.nominalUnionVariant( + scrutinee.type, + scrutinee.type, + scrutinee.variant, + scrutinee.variantOrdinal, + ) + } else if ( + scrutinee._tag === 'UnionValue' && + scrutinee.payload._tag === 'NominalUnionValue' + ) { + activeIdentity = Match.nominalUnionVariant( + scrutinee.member, + scrutinee.payload.type, + scrutinee.payload.variant, + scrutinee.payload.variantOrdinal, + ) + } let activeMember: Type.Type if (activeIdentity !== undefined) { activeMember = activeIdentity.type @@ -1950,13 +1967,18 @@ function* executeFunction( activeMember = scrutinee.member } else if (scrutinee._tag === 'AggregateValue') { activeMember = scrutinee.type + } else if (scrutinee._tag === 'NominalUnionValue') { + activeMember = scrutinee.type } else { activeMember = Mir.semanticType(operation.scrutineeType) } let payload: Value if (scrutinee._tag === 'UnionValue') { payload = scrutinee.payload - } else if (scrutinee._tag === 'AggregateValue') { + } else if ( + scrutinee._tag === 'AggregateValue' || + scrutinee._tag === 'NominalUnionValue' + ) { payload = scrutinee } else { payload = scrutinee diff --git a/packages/compiler/src/BootstrapStorage.ts b/packages/compiler/src/BootstrapStorage.ts index 1a15a47ef..9b4127edb 100644 --- a/packages/compiler/src/BootstrapStorage.ts +++ b/packages/compiler/src/BootstrapStorage.ts @@ -1,4 +1,4 @@ -import type { AggregateValue, Value } from './BootstrapValue.js' +import type { AggregateValue, NominalUnionValue, Value } from './BootstrapValue.js' import type * as CleanupPlan from './CleanupPlan.js' import * as DeclarationFacts from './DeclarationFacts.js' import type * as ExecutionPackage from './ExecutionPackage.js' @@ -257,9 +257,12 @@ export const selectFieldPath = ( ): Value => { let selected: Value = root for (const selector of path) { - if (selected._tag !== 'AggregateValue') - throw new RangeError('MIR verifier allowed a match field below a non-struct value') - const field: AggregateValue['fields'][number] | undefined = selected.fields.find((candidate) => + if (selected._tag !== 'AggregateValue' && selected._tag !== 'NominalUnionValue') + throw new RangeError('MIR verifier allowed a match field below a non-aggregate value') + const field: + | AggregateValue['fields'][number] + | NominalUnionValue['fields'][number] + | undefined = selected.fields.find((candidate) => DeclarationFacts.sameFieldId(candidate.field, selector), ) if (field === undefined) throw new RangeError('MIR verifier allowed a missing match field') diff --git a/packages/compiler/src/Elaboration.ts b/packages/compiler/src/Elaboration.ts index b8a454a17..59fbac7ba 100644 --- a/packages/compiler/src/Elaboration.ts +++ b/packages/compiler/src/Elaboration.ts @@ -440,6 +440,19 @@ export type PatternFact = readonly complete: boolean readonly syntax: SyntaxTree.Node } + | { + readonly _tag: 'UnionVariantPattern' + readonly id: Match.PatternId + readonly target: UnionVariantTargetFact + readonly member?: Type.Nominal + readonly coverage?: Match.CoverageIdentity + readonly fields: ReadonlyArray + readonly bindings: ReadonlyArray + readonly omitted: ReadonlyArray> + readonly rest: boolean + readonly complete: boolean + readonly syntax: SyntaxTree.Node + } | { readonly _tag: 'UniversalPattern' readonly id: Match.PatternId diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index c369cb50f..0e29bcdfd 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -461,6 +461,51 @@ export const enumFactByType = ( ) : undefined +export const unionFactByType = ( + index: DeclarationIndex.Index, + type: SemanticType, +): DeclarationFacts.UnionFact | undefined => { + if (!Type.isNominal(type)) return undefined + const declaration = DeclarationFacts.byCanonical(index, { + _tag: 'CanonicalDeclarationId', + module: type.module, + name: type.name, + }) + return declaration?._tag === 'UnionDeclaration' ? declaration : undefined +} + +const nominalUnionCoverage = ( + index: DeclarationIndex.Index, + type: Type.Nominal, + root: Type.Type, +): ReadonlyArray => { + const union = unionFactByType(index, type) + return Object.freeze( + union?.variants.flatMap((variant) => + variant.canonical._tag === 'Canonical' + ? [Match.nominalUnionVariant(root, type, variant.canonical.id, variant.id.ordinal)] + : [], + ) ?? [], + ) +} + +export const coverageMembersOf = ( + index: DeclarationIndex.Index, + type: Type.Type, +): ReadonlyArray => { + if (Type.isUnion(type)) + return Object.freeze( + type.members.flatMap((member) => + Type.isNominal(member) && unionFactByType(index, member) !== undefined + ? nominalUnionCoverage(index, member, member) + : [Match.structuralMember(member)], + ), + ) + if (Type.isNominal(type) && unionFactByType(index, type) !== undefined) + return nominalUnionCoverage(index, type, type) + return Match.membersOf(type) +} + export const analyzeEnumMember = ( source: SourceFile.SourceFile, node: SyntaxTree.Node, @@ -1598,7 +1643,12 @@ export const analyzePattern = ( }) } - if (node.kind === 'EnumMemberPattern') { + const bareUnionPatternTarget = + node.kind === 'EnumMemberPattern' || node.kind === 'NominalPattern' + ? resolveBareUnionVariantTarget(source, node, resolution) + : undefined + + if (node.kind === 'EnumMemberPattern' && bareUnionPatternTarget === undefined) { const identifiers = SyntaxTree.tokens(node).filter((token) => token.kind === 'Identifier') const qualifierToken = identifiers.at(0) const memberToken = identifiers.at(1) @@ -1732,19 +1782,61 @@ export const analyzePattern = ( }) } - const targetSyntax = SyntaxTree.directNode(node, 'AppliedType') ?? childNode(node, 'TypePath') - const target = resolveStructTarget(source, targetSyntax, resolution, declaration) + const variantSelector = SyntaxTree.directNode(node, 'UnionVariantSelector') + const unionTarget = + variantSelector === undefined + ? bareUnionPatternTarget + : resolveUnionVariantTarget(source, variantSelector, resolution, declaration) + let targetSyntax: SyntaxTree.Node + if (variantSelector === undefined && unionTarget !== undefined) { + targetSyntax = node + } else if (variantSelector === undefined) { + targetSyntax = SyntaxTree.directNode(node, 'AppliedType') ?? childNode(node, 'TypePath') + } else { + targetSyntax = + SyntaxTree.directNode(variantSelector, 'AppliedType') ?? + childNode(variantSelector, 'TypePath') + } + const structTarget = + unionTarget === undefined + ? resolveStructTarget(source, targetSyntax, resolution, declaration) + : undefined + const target = unionTarget ?? structTarget + if (target === undefined) throw new RangeError('Pattern target resolution is unavailable') const diagnostics: Array = [...target.diagnostics] - const struct = target.fact._tag === 'Resolved' ? target.fact.struct : undefined + const struct = + target.fact._tag === 'Resolved' && 'struct' in target.fact ? target.fact.struct : undefined + const union = + target.fact._tag === 'Resolved' && 'union' in target.fact ? target.fact.union : undefined + const variant = + target.fact._tag === 'Resolved' && 'variant' in target.fact ? target.fact.variant : undefined + const aggregate = struct ?? union + const aggregateFields = struct?.fields ?? variant?.fields ?? Object.freeze([]) const nominal = target.fact._tag === 'Resolved' ? target.fact.type : undefined const structSubstitution = - struct === undefined || nominal === undefined + aggregate === undefined || nominal === undefined ? new Map() : (TypeInference.substitution( - struct.typeParameters.map((parameter) => parameter.type), + aggregate.typeParameters.map((parameter) => parameter.type), nominal.arguments, ) ?? new Map()) - const label = nominal === undefined ? 'unknown struct' : Type.encode(nominal) + const unresolvedParameters = + aggregate === undefined || nominal === undefined + ? [] + : aggregate.typeParameters.filter((parameter, ordinal) => { + const argument = nominal.arguments.at(ordinal) + return argument === undefined || isOwnStructArgument(parameter.type, argument) + }) + for (const parameter of unresolvedParameters) { + diagnostics.push( + Diagnostic.uninferredTypeParameter( + nominal === undefined ? 'unknown aggregate' : Type.encode(nominal), + parameter.type.name, + parameter.syntax.span, + ), + ) + } + const label = nominal === undefined ? 'unknown aggregate' : Type.encode(nominal) const seen = new Map() const bindings: Array = [] const fields = SyntaxTree.directNodes(node, 'PatternField').map((fieldNode): PatternFieldFact => { @@ -1755,9 +1847,9 @@ export const analyzePattern = ( const nameToken = identifiers.at(0) const name = nameToken === undefined ? undefined : spelling(source, nameToken) const lookup = - struct === undefined || name === undefined + aggregate === undefined || name === undefined ? undefined - : DeclarationFacts.lookupField(struct.fields, name) + : DeclarationFacts.lookupField(aggregateFields, name) let state: PatternFieldState = Object.freeze({ _tag: 'Unavailable' }) let resolvedField: DeclarationFacts.FieldFact | undefined if (lookup?._tag === 'Resolved') { @@ -1789,6 +1881,7 @@ export const analyzePattern = ( } const nestedNode = + SyntaxTree.directNode(fieldNode, 'UnionVariantPattern') ?? SyntaxTree.directNode(fieldNode, 'NominalPattern') ?? SyntaxTree.directNode(fieldNode, 'BindingPattern') let nested: PatternFact | undefined @@ -1815,7 +1908,9 @@ export const analyzePattern = ( : undefined if ( expected !== undefined && - (nested._tag === 'NominalPattern' || nested._tag === 'TypePattern') && + (nested._tag === 'NominalPattern' || + nested._tag === 'UnionVariantPattern' || + nested._tag === 'TypePattern') && nested.member !== undefined && !Type.equals(expected, nested.member) ) { @@ -1887,37 +1982,62 @@ export const analyzePattern = ( const omitted: Array> = fields.flatMap( (field) => field.nested?.omitted ?? [], ) - if (struct !== undefined && !rest) { - for (const field of struct.fields) { + if (aggregate !== undefined && !rest) { + for (const field of aggregateFields) { if (field.name._tag !== 'Present' || seen.has(field.name.spelling)) continue diagnostics.push(Diagnostic.missingPatternField(label, field.name.spelling, node.span)) } - } else if (struct !== undefined && rest) { - for (const field of struct.fields) { + } else if (aggregate !== undefined && rest) { + for (const field of aggregateFields) { if (field.name._tag === 'Present' && seen.has(field.name.spelling)) continue omitted.push(Object.freeze([...prefix, field.id])) } } const complete = target.fact._tag === 'Resolved' && + unresolvedParameters.length === 0 && !counters.invalid && isAvailableSyntax(node) && fields.every( (field) => field.state._tag === 'Resolved' && (field.nested === undefined || - ((field.nested._tag === 'NominalPattern' || field.nested._tag === 'TypePattern') && + ((field.nested._tag === 'NominalPattern' || + field.nested._tag === 'UnionVariantPattern' || + field.nested._tag === 'TypePattern') && field.nested.complete)), ) && (rest || - struct?.fields.every( + aggregateFields.every( (field) => field.name._tag !== 'Present' || seen.has(field.name.spelling), ) === true) + if (unionTarget !== undefined) { + const coverage = + nominal !== undefined && variant?.canonical._tag === 'Canonical' + ? Match.nominalUnionVariant(nominal, nominal, variant.canonical.id, variant.id.ordinal) + : undefined + return Object.freeze({ + fact: Object.freeze({ + _tag: 'UnionVariantPattern', + id, + target: unionTarget.fact, + ...(nominal === undefined ? {} : { member: nominal }), + ...(coverage === undefined ? {} : { coverage }), + fields: Object.freeze(fields), + bindings: Object.freeze(bindings), + omitted: Object.freeze(omitted), + rest, + complete, + syntax: node, + }), + diagnostics: Object.freeze(diagnostics), + }) + } return Object.freeze({ fact: Object.freeze({ _tag: 'NominalPattern', id, - target: target.fact, + target: structTarget?.fact ?? Object.freeze({ _tag: 'Unavailable' }), ...(nominal === undefined ? {} : { member: nominal }), fields: Object.freeze(fields), bindings: Object.freeze(bindings), @@ -1975,7 +2095,7 @@ export const analyzeMatch = ( if (scrutinee?.type === undefined) { members = undefined } else if (enumScrutinee === undefined) { - members = Match.membersOf(scrutinee.type) + members = coverageMembersOf(resolution.index, scrutinee.type) } else { members = Match.enumMembersOf(enumScrutinee) } @@ -1986,6 +2106,7 @@ export const analyzeMatch = ( SyntaxTree.directNode(armNode, 'ErrorPattern') ?? SyntaxTree.directNode(armNode, 'EnumMemberPattern') ?? SyntaxTree.directNode(armNode, 'IntegerPattern') ?? + SyntaxTree.directNode(armNode, 'UnionVariantPattern') ?? SyntaxTree.directNode(armNode, 'NominalPattern') ?? SyntaxTree.directNode(armNode, 'BindingPattern') ?? SyntaxTree.directNode(armNode, 'UniversalPattern') @@ -2010,6 +2131,7 @@ export const analyzeMatch = ( }) const coverageIdentity = (pattern: PatternFact): Match.CoverageIdentity | undefined => { if (pattern._tag === 'EnumMemberPattern') return pattern.coverage + if (pattern._tag === 'UnionVariantPattern') return pattern.coverage return (pattern._tag === 'NominalPattern' || pattern._tag === 'TypePattern') && pattern.member !== undefined ? Match.structuralMember(pattern.member) @@ -2056,10 +2178,10 @@ export const analyzeMatch = ( const member = coverageIdentity(pattern) const guarded = directToken(armNode, 'IfKeyword') !== undefined const memberInDomain = - member !== undefined && members?.some((candidate) => Match.identityEquals(candidate, member)) + member !== undefined && members?.some((candidate) => Match.selects(member, candidate)) if ( enumScrutinee === undefined && - member?._tag === 'StructuralTypeMember' && + member !== undefined && members !== undefined && !memberInDomain ) { @@ -2296,7 +2418,9 @@ export const analyzeMatch = ( arm.reachable && (arm.pattern._tag === 'UniversalPattern' || (arm.pattern._tag === 'EnumMemberPattern' && arm.pattern.complete) || - ((arm.pattern._tag === 'NominalPattern' || arm.pattern._tag === 'TypePattern') && + ((arm.pattern._tag === 'NominalPattern' || + arm.pattern._tag === 'UnionVariantPattern' || + arm.pattern._tag === 'TypePattern') && arm.pattern.complete)), ) && !unavailableReachableResult && diff --git a/packages/compiler/src/HirLowering.ts b/packages/compiler/src/HirLowering.ts index 20ec54985..c6b223f60 100644 --- a/packages/compiler/src/HirLowering.ts +++ b/packages/compiler/src/HirLowering.ts @@ -70,6 +70,8 @@ export const hirPatternSelection = (selection: PatternSelectionFact): Hir.Patter let member: Match.CoverageIdentity | undefined if (selection.pattern._tag === 'EnumMemberPattern') { member = selection.pattern.coverage + } else if (selection.pattern._tag === 'UnionVariantPattern') { + member = selection.pattern.coverage } else if ( (selection.pattern._tag === 'NominalPattern' || selection.pattern._tag === 'TypePattern') && selection.pattern.member !== undefined @@ -659,6 +661,8 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. let member: Match.CoverageIdentity | undefined if (arm.pattern._tag === 'EnumMemberPattern') { member = arm.pattern.coverage + } else if (arm.pattern._tag === 'UnionVariantPattern') { + member = arm.pattern.coverage } else if ( (arm.pattern._tag === 'NominalPattern' || arm.pattern._tag === 'TypePattern') && arm.pattern.member !== undefined diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 9004a7464..b583b9173 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -4181,6 +4181,70 @@ export const memberFieldSlots = ( ) } +/** Canonical match leaves described by a realized calling shape. */ +export const coverageMembers = (shape: CallingShape): ReadonlyArray => { + const variants = ( + root: Type.Type, + node: Extract, + ): ReadonlyArray => + node.variants.map((variant) => + Match.nominalUnionVariant(root, node.type, variant.variant, variant.ordinal), + ) + if (shape.tree._tag === 'NominalUnionShape') + return Object.freeze(variants(shape.type, shape.tree)) + if (shape.tree._tag !== 'SumShape') return Match.membersOf(shape.type) + return Object.freeze( + shape.tree.members.flatMap((member) => + member.shape._tag === 'NominalUnionShape' + ? variants(member.member, member.shape) + : [Match.structuralMember(member.member)], + ), + ) +} + +/** Physical calling-lane slots for a field selected by one exact match coverage identity. */ +export const coverageFieldSlots = ( + shape: CallingShape, + member: Match.CoverageIdentity, + path: ReadonlyArray, +): ReadonlyArray | undefined => { + if (member._tag !== 'NominalUnionVariant') + return memberFieldSlots(shape, Match.sourceType(member), path) + let selected: { readonly shape: CallingShapeNode; readonly physicalOffset: number } | undefined + if (shape.tree._tag === 'NominalUnionShape' && Type.equals(shape.tree.type, member.type)) { + const variant = shape.tree.variants.find( + (candidate) => + candidate.ordinal === member.variantOrdinal && + candidate.variant.union.module === member.variant.union.module && + candidate.variant.union.name === member.variant.union.name && + candidate.variant.name === member.variant.name, + ) + if (variant !== undefined) selected = { shape: variant.shape, physicalOffset: 1 } + } else if (shape.tree._tag === 'SumShape') { + const outer = shape.tree.members.find((candidate) => Type.equals(candidate.member, member.root)) + if (outer?.shape._tag === 'NominalUnionShape') { + const variant = outer.shape.variants.find( + (candidate) => + candidate.ordinal === member.variantOrdinal && + candidate.variant.union.module === member.variant.union.module && + candidate.variant.union.name === member.variant.union.name && + candidate.variant.name === member.variant.name, + ) + if (variant !== undefined) selected = { shape: variant.shape, physicalOffset: 2 } + } + } + if (selected === undefined) return undefined + const slice = fieldSlice(selected.shape, path) + return slice === undefined + ? undefined + : Object.freeze( + Array.from( + { length: slice.length }, + (_, ordinal) => selected.physicalOffset + slice.offset + ordinal, + ), + ) +} + /** Looks up one available or unavailable nominal catalog entry. */ export const catalogEntry = ( self: Catalog, diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index ea19fd9dc..8ac4a0278 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -1372,10 +1372,20 @@ export function lowerExpressionInner( candidate.id.span.start === expression.id.span.start && candidate.id.span.end === expression.id.span.end, ) - const specializeMember = (member: Match.CoverageIdentity): Match.CoverageIdentity => - member._tag === 'StructuralTypeMember' - ? Match.structuralMember(fn.semantic(member.type)) + const specializeMember = (member: Match.CoverageIdentity): Match.CoverageIdentity => { + if (member._tag === 'StructuralTypeMember') + return Match.structuralMember(fn.semantic(member.type)) + if (member._tag !== 'NominalUnionVariant') return member + const type = fn.semantic(member.type) + return Type.isNominal(type) + ? Match.nominalUnionVariant( + fn.semantic(member.root), + type, + member.variant, + member.variantOrdinal, + ) : member + } const members = Object.freeze(expression.members.map(specializeMember)) const specializedCoverage = Match.cover( members, @@ -1495,8 +1505,7 @@ export function lowerExpressionInner( arms .filter( (arm) => - arm.universal || - (arm.member !== undefined && Match.identityEquals(arm.member, member)), + arm.universal || (arm.member !== undefined && Match.selects(arm.member, member)), ) .map((arm) => arm.id), ), diff --git a/packages/compiler/src/LowerStatements.ts b/packages/compiler/src/LowerStatements.ts index 5425d0b72..a3e3565a3 100644 --- a/packages/compiler/src/LowerStatements.ts +++ b/packages/compiler/src/LowerStatements.ts @@ -66,16 +66,23 @@ export const lowerPatternSelection = ( const resultType = fn.type(resultSemantic) if (subject === undefined || subjectType === undefined || resultType === undefined) return undefined - if ( - selection.members.some((member) => member._tag === 'EnumMember') || - selection.member?._tag === 'EnumMember' - ) - return undefined - const members = Match.membersOf(semanticSubject) - const member = - selection.member?._tag === 'StructuralTypeMember' - ? Match.structuralMember(fn.semantic(selection.member.type)) - : undefined + if (selection.members.some((member) => member._tag === 'EnumMember')) return undefined + const specializeMember = (candidate: Match.CoverageIdentity): Match.CoverageIdentity => { + if (candidate._tag === 'StructuralTypeMember') + return Match.structuralMember(fn.semantic(candidate.type)) + if (candidate._tag !== 'NominalUnionVariant') return candidate + const type = fn.semantic(candidate.type) + return Type.isNominal(type) + ? Match.nominalUnionVariant( + fn.semantic(candidate.root), + type, + candidate.variant, + candidate.variantOrdinal, + ) + : candidate + } + const members = Object.freeze(selection.members.map(specializeMember)) + const member = selection.member === undefined ? undefined : specializeMember(selection.member) const literal = (value: boolean): LoweredExpression | undefined => lowerExpression( fn, @@ -120,9 +127,7 @@ export const lowerPatternSelection = ( const selectedAfter = selection.universal ? emptyCoverage : Object.freeze( - members.filter( - (candidate) => member === undefined || !Match.identityEquals(candidate, member), - ), + members.filter((candidate) => member === undefined || !Match.selects(member, candidate)), ) const ownedArm = ownership?.arms.find( (candidate) => candidate.id.ordinal === selection.arm.ordinal, @@ -193,7 +198,7 @@ export const lowerPatternSelection = ( let candidates: ReadonlyArray if (selection.universal) { candidates = [selection.arm] - } else if (member === undefined || !Match.identityEquals(candidate, member)) { + } else if (member === undefined || !Match.selects(member, candidate)) { candidates = [fallbackId] } else { candidates = needsFallback ? [selection.arm, fallbackId] : [selection.arm] diff --git a/packages/compiler/src/Match.ts b/packages/compiler/src/Match.ts index 480e33109..48327bebb 100644 --- a/packages/compiler/src/Match.ts +++ b/packages/compiler/src/Match.ts @@ -37,6 +37,13 @@ export interface BindingId { /** One exact inhabitant in a closed match coverage domain. */ export type CoverageIdentity = | { readonly _tag: 'StructuralTypeMember'; readonly type: Type.Type } + | { + readonly _tag: 'NominalUnionVariant' + readonly root: Type.Type + readonly type: Type.Nominal + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly variantOrdinal: number + } | { readonly _tag: 'EnumMember' readonly enum: DeclarationFacts.CanonicalId @@ -60,23 +67,48 @@ export const enumMember = ( type: Type.nominal(enum_.module, enum_.name), }) +/** Creates one leaf selection beneath a nominal-union parent and optional structural root. */ +export const nominalUnionVariant = ( + root: Type.Type, + type: Type.Nominal, + variant: DeclarationFacts.CanonicalUnionVariantId, + variantOrdinal: number, +): CoverageIdentity => + Object.freeze({ _tag: 'NominalUnionVariant', root, type, variant, variantOrdinal }) + /** Tests canonical coverage identity without erasing enum members to types or integers. */ -export const identityEquals = (self: CoverageIdentity, other: CoverageIdentity): boolean => - self._tag === 'StructuralTypeMember' - ? other._tag === 'StructuralTypeMember' && Type.equals(self.type, other.type) - : other._tag === 'EnumMember' && - self.enum.module === other.enum.module && - self.enum.name === other.enum.name && - self.member.name === other.member.name +export const identityEquals = (self: CoverageIdentity, other: CoverageIdentity): boolean => { + if (self._tag === 'StructuralTypeMember') + return other._tag === 'StructuralTypeMember' && Type.equals(self.type, other.type) + if (self._tag === 'NominalUnionVariant') + return ( + other._tag === 'NominalUnionVariant' && + Type.equals(self.root, other.root) && + Type.equals(self.type, other.type) && + self.variant.union.module === other.variant.union.module && + self.variant.union.name === other.variant.union.name && + self.variant.name === other.variant.name && + self.variantOrdinal === other.variantOrdinal + ) + return ( + other._tag === 'EnumMember' && + self.enum.module === other.enum.module && + self.enum.name === other.enum.name && + self.member.name === other.member.name + ) +} /** Returns the source type selected by one coverage identity. */ -export const sourceType = (self: CoverageIdentity): Type.Type => self.type +export const sourceType = (self: CoverageIdentity): Type.Type => + self._tag === 'NominalUnionVariant' ? self.root : self.type /** Encodes one coverage identity for diagnostics and deterministic snapshots. */ -export const encodeIdentity = (self: CoverageIdentity): string => - self._tag === 'StructuralTypeMember' - ? Type.encode(self.type) - : `${self.enum.module}.${self.enum.name}.${self.member.name}` +export const encodeIdentity = (self: CoverageIdentity): string => { + if (self._tag === 'StructuralTypeMember') return Type.encode(self.type) + if (self._tag === 'NominalUnionVariant') + return `${Type.encode(self.root)}::${Type.encode(self.type)}.${self.variant.name}` + return `${self.enum.module}.${self.enum.name}.${self.member.name}` +} /** One source decision reduced to the facts that affect coverage. */ export interface Decision { @@ -101,7 +133,14 @@ export interface Coverage { } const contains = (members: ReadonlyArray, member: CoverageIdentity): boolean => - members.some((candidate) => identityEquals(candidate, member)) + members.some((candidate) => selects(member, candidate)) + +/** Tests whether one authored pattern identity selects one canonical coverage leaf. */ +export const selects = (pattern: CoverageIdentity, candidate: CoverageIdentity): boolean => + identityEquals(pattern, candidate) || + (pattern._tag === 'StructuralTypeMember' && + candidate._tag === 'NominalUnionVariant' && + Type.equals(pattern.type, candidate.root)) /** Returns the canonical structural exact-member set observed by a pattern decision. */ export const membersOf = (type: Type.Type): ReadonlyArray => { @@ -142,8 +181,7 @@ export const cover = ( ? Object.freeze([]) : Object.freeze( before.filter( - (candidate) => - decision.member === undefined || !identityEquals(candidate, decision.member), + (candidate) => decision.member === undefined || !selects(decision.member, candidate), ), ) } diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index 2184b0911..f17f83b6d 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -1,8 +1,7 @@ import type { ControlProvenance } from './Backend.js' import type * as Layout from './Layout.js' -import * as Match from './Match.js' +import type * as Match from './Match.js' import * as Mir from './Mir.js' -import type * as SilkType from './Type.js' export type LinearTerminator = | { readonly _tag: 'Return'; readonly value: Mir.LocalId; readonly provenance: Mir.Provenance } @@ -18,7 +17,8 @@ export type LinearTerminator = | { readonly _tag: 'MatchBranch' readonly scrutinee: Mir.LocalId - readonly memberOrdinal: number + readonly shape: Layout.CallingShape + readonly member: Match.CoverageIdentity readonly taken: Mir.RegionId readonly otherwise: Mir.RegionId readonly provenance: Mir.Provenance @@ -41,7 +41,7 @@ export type LinearOperation = readonly _tag: 'BindMatch' readonly scrutinee: Mir.LocalId readonly shape: Layout.CallingShape - readonly member: SilkType.Type + readonly member: Match.CoverageIdentity readonly binding: Mir.MatchBinding readonly provenance: Mir.Provenance } @@ -350,7 +350,7 @@ export const expandMatches = ( _tag: 'BindMatch' as const, scrutinee: match.scrutinee, shape: match.scrutineeShape, - member: Match.sourceType(member), + member, binding, provenance: binding.provenance, }), @@ -434,7 +434,10 @@ export const expandMatches = ( }) return } - if (match.scrutineeType._tag !== 'Union') { + if ( + match.scrutineeShape.tree._tag !== 'SumShape' && + match.scrutineeShape.tree._tag !== 'NominalUnionShape' + ) { const selected = decisionEntries.at(0) ?? trap blocks.push( Object.freeze({ @@ -448,7 +451,7 @@ export const expandMatches = ( return } const dispatchIds = match.decisions.map((_, ordinal) => (ordinal === 0 ? dispatch : reserve())) - match.decisions.forEach((_, ordinal) => { + match.decisions.forEach((decision, ordinal) => { blocks.push( Object.freeze({ id: dispatchIds.at(ordinal) ?? dispatch, @@ -458,7 +461,8 @@ export const expandMatches = ( terminator: Object.freeze({ _tag: 'MatchBranch', scrutinee: match.scrutinee, - memberOrdinal: ordinal, + shape: match.scrutineeShape, + member: decision.member, taken: decisionEntries.at(ordinal) ?? trap, otherwise: dispatchIds.at(ordinal + 1) ?? trap, provenance: match.provenance, diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 60e84a6ad..d74be34e7 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -938,6 +938,40 @@ const fieldPathType = ( return current } +const coverageFieldPathType = ( + layout: Layout.Plan, + member: Match.CoverageIdentity, + path: ReadonlyArray, +): DeclarationFacts.SemanticType | undefined => { + if (member._tag !== 'NominalUnionVariant') + return fieldPathType(layout, Match.sourceType(member), path) + let current: DeclarationFacts.SemanticType | undefined = member.type + let variant = Layout.entry(layout, member.type)?.representation + for (const [ordinal, selector] of path.entries()) { + let field: Layout.Field | undefined + if (ordinal === 0 && variant?._tag === 'NominalUnion') { + field = variant.variants + .find((candidate) => candidate.ordinal === member.variantOrdinal) + ?.fields.find((candidate) => DeclarationFacts.sameFieldId(candidate.id, selector)) + } else if (SilkType.isNominal(current)) { + const representation: Layout.Representation | undefined = Layout.entry( + layout, + current, + )?.representation + field = + representation?._tag === 'Aggregate' + ? representation.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, selector), + ) + : undefined + } + current = field?.type + variant = undefined + if (current === undefined) return undefined + } + return current +} + const sameMembers = ( left: ReadonlyArray, right: ReadonlyArray, @@ -4261,9 +4295,12 @@ export const verify = (self: Module): ReadonlyArray => { operation.arms.every( (arm) => arm.member === undefined || arm.member._tag === 'EnumMember', )) + const plannedMembers = Layout.coverageMembers(operation.scrutineeShape) const decisionsValid = coverage.exhaustive && enumCoverageValid && + (operation.scrutineeType._tag === 'Enum' || + sameCoverage(operation.members, plannedMembers)) && operation.decisions.length === operation.members.length && operation.decisions.every((decision, ordinal) => { const member = operation.members.at(ordinal) @@ -4272,7 +4309,7 @@ export const verify = (self: Module): ReadonlyArray => { arm.universal || (arm.member !== undefined && member !== undefined && - Match.identityEquals(arm.member, member)), + Match.selects(arm.member, member)), ) return ( member !== undefined && @@ -4310,7 +4347,7 @@ export const verify = (self: Module): ReadonlyArray => { const selected = arm.member === undefined ? undefined - : fieldPathType(self.layout, Match.sourceType(arm.member), binding.path) + : coverageFieldPathType(self.layout, arm.member, binding.path) if ( localType === undefined || selected === undefined || @@ -4369,7 +4406,7 @@ export const verify = (self: Module): ReadonlyArray => { const selected = arm.member === undefined ? undefined - : fieldPathType(self.layout, Match.sourceType(arm.member), entry.path) + : coverageFieldPathType(self.layout, arm.member, entry.path) return selected !== undefined && SilkType.equals(selected, entry.cleanup.type) }) : arm.selected.cleanup.length === 0) diff --git a/packages/compiler/src/NativeControl.ts b/packages/compiler/src/NativeControl.ts index fc18cda8c..58410ea88 100644 --- a/packages/compiler/src/NativeControl.ts +++ b/packages/compiler/src/NativeControl.ts @@ -8,6 +8,7 @@ import type * as LlvmType from '@silklang/llvm/Type' import type * as Value from '@silklang/llvm/Value' import * as Effect from 'effect/Effect' import * as CleanupPlan from './CleanupPlan.js' +import * as Match from './Match.js' import * as Mir from './Mir.js' import type { LinearTerminator } from './MirLinearization.js' import * as NativeAggregate from './NativeAggregate.js' @@ -15,6 +16,7 @@ import * as NativeDebug from './NativeDebug.js' import type * as NativeLoweringContext from './NativeLoweringContext.js' import * as NativeSuspension from './NativeSuspension.js' import * as NativeType from './NativeType.js' +import * as SilkType from './Type.js' export interface Context { readonly builder: Builder.Builder @@ -115,20 +117,64 @@ export const matchBranch = Effect.fnUntraced(function* ( terminator: Extract, blockOrdinal: number, ): Effect.fn.Return { - const tag = read(context, terminator.scrutinee).at(0) - if (tag === undefined) throw new RangeError('LLVM union match has no tag lane') - const expected = yield* Constant.integerSigned( - context.builder, - context.i32, - BigInt(terminator.memberOrdinal), - ) - const condition = yield* FunctionBody.integerCompare( - context.body, - 'eq', - tag, - expected, - `match${blockOrdinal}_member`, - ) + const values = read(context, terminator.scrutinee) + const tag = values.at(0) + if (tag === undefined) throw new RangeError('LLVM match has no tag lane') + let condition: Value.Value + if (terminator.member._tag === 'NominalUnionVariant') { + const member = terminator.member + const nested = terminator.shape.tree._tag === 'SumShape' + const variantTag = values.at(nested ? 1 : 0) + if (variantTag === undefined) throw new RangeError('LLVM nominal union match has no tag lane') + const variantMatches = yield* FunctionBody.integerCompare( + context.body, + 'eq', + variantTag, + yield* Constant.integerSigned(context.builder, context.i32, BigInt(member.variantOrdinal)), + `match${blockOrdinal}_variant`, + ) + if (!nested) { + condition = variantMatches + } else { + const outer = + terminator.shape.tree._tag === 'SumShape' + ? terminator.shape.tree.members.find((candidate) => + SilkType.equals(candidate.member, member.root), + ) + : undefined + if (outer === undefined) + throw new RangeError('LLVM nominal union match lost its structural member') + const rootMatches = yield* FunctionBody.integerCompare( + context.body, + 'eq', + tag, + yield* Constant.integerSigned(context.builder, context.i32, BigInt(outer.ordinal)), + `match${blockOrdinal}_root`, + ) + condition = yield* FunctionBody.binary( + context.body, + 'and', + rootMatches, + variantMatches, + `match${blockOrdinal}_member`, + ) + } + } else { + const outer = + terminator.shape.tree._tag === 'SumShape' + ? terminator.shape.tree.members.find((candidate) => + SilkType.equals(candidate.member, Match.sourceType(terminator.member)), + ) + : undefined + if (outer === undefined) throw new RangeError('LLVM union match lost its structural member') + condition = yield* FunctionBody.integerCompare( + context.body, + 'eq', + tag, + yield* Constant.integerSigned(context.builder, context.i32, BigInt(outer.ordinal)), + `match${blockOrdinal}_member`, + ) + } yield* FunctionBody.conditionalBranch( context.body, condition, diff --git a/packages/compiler/src/NativeValueOperation.ts b/packages/compiler/src/NativeValueOperation.ts index 20c331981..0e08de255 100644 --- a/packages/compiler/src/NativeValueOperation.ts +++ b/packages/compiler/src/NativeValueOperation.ts @@ -48,7 +48,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op const checkOrdinal = context.state.checkOrdinal switch (operation._tag) { case 'BindMatch': { - const physical = Layout.memberFieldSlots( + const physical = Layout.coverageFieldSlots( operation.shape, operation.member, operation.binding.path, diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index 514071a77..d8aec00ef 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -824,6 +824,43 @@ const collectPattern = ( ) if (field.nested !== undefined) collectPattern(field.nested, index, scope, pending) } + } else if (pattern._tag === 'UnionVariantPattern') { + if (pattern.target._tag === 'Resolved') { + const parentToken = SyntaxTree.tokens(pattern.syntax).find( + (token) => token.kind === 'Identifier', + ) + push( + pending, + parentToken?.span, + 'Type', + available(identityOfDeclaration(pattern.target.union)), + locationOfDeclaration(index, pattern.target.union), + ) + if (pattern.target.variant.canonical._tag === 'Canonical') + push( + pending, + pattern.target.token.span, + 'Value', + available( + Object.freeze({ + _tag: 'UnionVariantIdentity', + id: pattern.target.variant.canonical.id, + }), + ), + locationOfUnionVariant(pattern.target.variant), + ) + } + for (const field of pattern.fields) { + if (field.state._tag === 'Resolved') + push( + pending, + field.token?.span, + 'Field', + available(Object.freeze({ _tag: 'FieldIdentity', id: field.state.field.id })), + locationOfField(index, field.state.field), + ) + if (field.nested !== undefined) collectPattern(field.nested, index, scope, pending) + } } else if (pattern._tag === 'TypePattern') { collectDeclaredType(pattern.declared, index, scope, pending) } diff --git a/packages/compiler/src/StatementAnalysis.ts b/packages/compiler/src/StatementAnalysis.ts index 88cef4db3..809c80059 100644 --- a/packages/compiler/src/StatementAnalysis.ts +++ b/packages/compiler/src/StatementAnalysis.ts @@ -35,6 +35,7 @@ import { analyzeExpression, analyzePattern, bindingName, + coverageMembersOf, enumFactByType, statementExpressionNode, unsafeCallAuthorized, @@ -293,6 +294,7 @@ export const analyzeStatements = ( SyntaxTree.directNode(element, 'ErrorPattern') ?? SyntaxTree.directNode(element, 'EnumMemberPattern') ?? SyntaxTree.directNode(element, 'IntegerPattern') ?? + SyntaxTree.directNode(element, 'UnionVariantPattern') ?? SyntaxTree.directNode(element, 'NominalPattern') ?? SyntaxTree.directNode(element, 'BindingPattern') ?? SyntaxTree.directNode(element, 'UniversalPattern') @@ -317,13 +319,15 @@ export const analyzeStatements = ( if (subject.type._tag !== 'Available') { members = [] } else if (subjectEnum === undefined) { - members = Match.membersOf(subject.type.type) + members = coverageMembersOf(context.resolution.index, subject.type.type) } else { members = Match.enumMembersOf(subjectEnum) } let member: Match.CoverageIdentity | undefined if (pattern.fact._tag === 'EnumMemberPattern') { member = pattern.fact.coverage + } else if (pattern.fact._tag === 'UnionVariantPattern') { + member = pattern.fact.coverage } else if ( (pattern.fact._tag === 'NominalPattern' || pattern.fact._tag === 'TypePattern') && pattern.fact.member !== undefined @@ -334,9 +338,9 @@ export const analyzeStatements = ( } if ( subjectEnum === undefined && - member?._tag === 'StructuralTypeMember' && + member !== undefined && subject.type._tag === 'Available' && - !members.some((candidate) => Match.identityEquals(candidate, member)) + !members.some((candidate) => Match.selects(member, candidate)) ) { context.diagnostics.push( Diagnostic.matchMemberNotInScrutinee( @@ -599,6 +603,8 @@ export const analyzeStatements = ( let selected: Match.CoverageIdentity | undefined if (selection.pattern._tag === 'EnumMemberPattern') { selected = selection.pattern.coverage + } else if (selection.pattern._tag === 'UnionVariantPattern') { + selected = selection.pattern.coverage } else if ( (selection.pattern._tag === 'NominalPattern' || selection.pattern._tag === 'TypePattern') && @@ -618,7 +624,7 @@ export const analyzeStatements = ( ) : '', selection.members - .filter((member) => selected === undefined || !Match.identityEquals(member, selected)) + .filter((member) => selected === undefined || !Match.selects(selected, member)) .map(Match.encodeIdentity), selection.pattern.syntax.span, ), diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 0defe5915..d06ae5d79 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -4610,11 +4610,7 @@ const emitMatchOperation = ( const arm = operation.arms.find((entry) => entry.id.ordinal === candidate.ordinal) if (arm === undefined) throw new RangeError('Wasm match lost a candidate arm') const bindings = arm.bindings.flatMap((binding) => { - const physical = LayoutPlan.memberFieldSlots( - operation.scrutineeShape, - Match.sourceType(member), - binding.path, - ) + const physical = LayoutPlan.coverageFieldSlots(operation.scrutineeShape, member, binding.path) if (physical === undefined) { throw new RangeError('Wasm match lost a pattern payload path') } @@ -4665,12 +4661,43 @@ const emitMatchOperation = ( Instr.ifElse(Instr.emptyBlockType, selected, emitDecisions(ordinal + 1)), ] } - if (operation.scrutineeType._tag !== 'Union') return selected const tag = slots(operation.scrutinee).at(0) - if (tag === undefined) throw new RangeError('Wasm union match has no tag lane') + if (tag === undefined) throw new RangeError('Wasm match has no tag lane') + if (decision.member._tag === 'NominalUnionVariant') { + const member = decision.member + const nested = operation.scrutineeShape.tree._tag === 'SumShape' + const variantTag = slots(operation.scrutinee).at(nested ? 1 : 0) + if (variantTag === undefined) throw new RangeError('Wasm nominal union match has no tag lane') + const outer = + operation.scrutineeShape.tree._tag === 'SumShape' + ? operation.scrutineeShape.tree.members.find((candidate) => + SilkType.equals(candidate.member, member.root), + ) + : undefined + if (nested && outer === undefined) + throw new RangeError('Wasm nominal union match lost its structural member') + return [ + ...(nested + ? [Instr.localGet(tag), Instr.i32Const(outer?.ordinal ?? 0), Instr.op('i32.eq')] + : []), + Instr.localGet(variantTag), + Instr.i32Const(member.variantOrdinal), + Instr.op('i32.eq'), + ...(nested ? [Instr.op('i32.and')] : []), + Instr.ifElse(Instr.emptyBlockType, selected, emitDecisions(ordinal + 1)), + ] + } + if (operation.scrutineeType._tag !== 'Union') return selected + const outer = + operation.scrutineeShape.tree._tag === 'SumShape' + ? operation.scrutineeShape.tree.members.find((candidate) => + SilkType.equals(candidate.member, Match.sourceType(decision.member)), + ) + : undefined + if (outer === undefined) throw new RangeError('Wasm union match lost its structural member') return [ Instr.localGet(tag), - Instr.i32Const(ordinal), + Instr.i32Const(outer.ordinal), Instr.op('i32.eq'), Instr.ifElse(Instr.emptyBlockType, selected, emitDecisions(ordinal + 1)), ] diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 5a979dbf3..059ab129f 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -4,6 +4,7 @@ import * as Analysis from '../src/Analysis.js' import * as Hir from '../src/Hir.js' import * as Layout from '../src/Layout.js' import * as LayoutEncode from '../src/LayoutEncode.js' +import * as Match from '../src/Match.js' import * as MirEncoding from '../src/MirEncoding.js' import * as MirVerification from '../src/MirVerification.js' import * as SourceFile from '../src/SourceFile.js' @@ -246,6 +247,47 @@ pub fn main() -> i32 { let state = keep(make()) return 42 }`), }), ) +it.effect('evaluates exhaustive nominal union variant patterns with payload bindings', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/patterns', + ascii(`union Option { Some { value: T }, None } +fn unwrap(option: Option) -> i32 { + return match move option { + Option.Some { value } => value + Option.None => 0 + } +} +pub fn main() -> i32 { return unwrap(Option.Some { value: 42 }) }`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + const returned = Analysis.rootAnalysis(self).functions.at(0)?.returnedExpression + assert.strictEqual(returned?._tag, 'Match') + if (returned?._tag !== 'Match') return + assert.strictEqual(returned.exhaustive, true) + assert.deepEqual(returned.members.map(Match.encodeIdentity), [ + 'union-values/patterns.Option::union-values/patterns.Option.Some', + 'union-values/patterns.Option::union-values/patterns.Option.None', + ]) + assert.deepEqual( + returned.arms.map((arm) => [arm.pattern._tag, arm.bindings.length, arm.reachable]), + [ + ['UnionVariantPattern', 1, true], + ['UnionVariantPattern', 0, true], + ], + ) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(self)), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + it.effect('evaluates initializers in source order before constructing in declaration order', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( From a85cb36a4c250354c94e867cdcb7a82cd79a4c50 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 14:18:47 -0300 Subject: [PATCH 09/42] feat(compiler): clean active nominal union payloads --- openspec/changes/add-nominal-unions/tasks.md | 12 +- packages/compiler/src/BootstrapEvaluation.ts | 19 +++ packages/compiler/src/BootstrapStorage.ts | 18 +++ packages/compiler/src/CleanupPlan.ts | 73 ++++++++- packages/compiler/src/ExecutableOrigin.ts | 4 + .../compiler/src/InspectorProjectBackend.ts | 4 + packages/compiler/src/MirLinearization.ts | 8 +- packages/compiler/src/MirVerification.ts | 34 ++++ packages/compiler/src/NativeAggregate.ts | 67 ++++++++ packages/compiler/src/OwnershipEncoding.ts | 10 ++ .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 153 +++++++++++++++++- .../compiler/test/BoxHeapIndirection.test.ts | 40 +++++ packages/compiler/test/Ownership.test.ts | 24 +++ packages/compiler/test/StructValues.test.ts | 137 ++++++++++++++++ 15 files changed, 593 insertions(+), 12 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 139d4ae7f..73a82b2f8 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -27,12 +27,12 @@ ## 4. Variant Patterns and Hierarchical Coverage -- [ ] 4.1 Extend the shared pattern representation with fully applied variant selectors and struct-like named-field bindings, omissions, nesting, borrows, moves, and writes, and verify unit and payload pattern diagnostics match struct field policy. -- [ ] 4.2 Replace flat match coverage keys with canonical selection paths that retain structural roots, applied nominal parents, and variants, and verify ordinary structural-union and scalar-enum coverage behavior remains unchanged. -- [ ] 4.3 Implement direct variant subtraction through structural-union roots plus whole-parent subtree subtraction, and verify exhaustive, duplicate, unreachable, wildcard, and fully qualified missing-path diagnostics. -- [ ] 4.4 Preserve nominal union roots as atomic `A | B` members during injection, widening, normalization, pattern selection, and specialization, and verify matching a leaf never changes the structural member set. -- [ ] 4.5 Keep guarded affine variant selections provisional until guard success, and verify a false guard leaves both tag levels, complete payload ownership, and cleanup available to a later arm. -- [ ] 4.6 Cover generic and uninhabited cases, and verify `Option | Option` retains distinct fully applied paths and `Result.Failure` remains a required coverage leaf without becoming constructible. +- [x] 4.1 Extend the shared pattern representation with fully applied variant selectors and struct-like named-field bindings, omissions, nesting, borrows, moves, and writes, and verify unit and payload pattern diagnostics match struct field policy. +- [x] 4.2 Replace flat match coverage keys with canonical selection paths that retain structural roots, applied nominal parents, and variants, and verify ordinary structural-union and scalar-enum coverage behavior remains unchanged. +- [x] 4.3 Implement direct variant subtraction through structural-union roots plus whole-parent subtree subtraction, and verify exhaustive, duplicate, unreachable, wildcard, and fully qualified missing-path diagnostics. +- [x] 4.4 Preserve nominal union roots as atomic `A | B` members during injection, widening, normalization, pattern selection, and specialization, and verify matching a leaf never changes the structural member set. +- [x] 4.5 Keep guarded affine variant selections provisional until guard success, and verify a false guard leaves both tag levels, complete payload ownership, and cleanup available to a later arm. +- [x] 4.6 Cover generic and uninhabited cases, and verify `Option | Option` retains distinct fully applied paths and `Result.Failure` remains a required coverage leaf without becoming constructible. ## 5. Ownership, Represented Fields, and Cleanup diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index 5a4b7d38c..e5c6987cc 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -944,6 +944,25 @@ function* executeFunction( } return undefined } + case 'NominalUnionCleanup': { + if (owner._tag !== 'NominalUnionValue') return undefined + const active = cleanup.variants.find((variant) => variant.ordinal === owner.variantOrdinal) + if (active === undefined) return undefined + for (const field of active.fields) { + const entry = owner.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.field, field.field), + ) + if (entry === undefined) continue + const blocked = yield* releaseThroughPlan( + field.cleanup, + entry.value, + provenance, + localOrdinal, + ) + if (blocked !== undefined) return blocked + } + return undefined + } case 'ArrayCleanup': { if (owner._tag !== 'ArrayValue') return undefined for (const element of owner.elements) { diff --git a/packages/compiler/src/BootstrapStorage.ts b/packages/compiler/src/BootstrapStorage.ts index 9b4127edb..753d7ee6e 100644 --- a/packages/compiler/src/BootstrapStorage.ts +++ b/packages/compiler/src/BootstrapStorage.ts @@ -239,6 +239,24 @@ export const cleanupMembers = ( if (cleanup._tag === 'HookCleanup') return cleanupMembers(cleanup.inner, owner) if (cleanup._tag === 'RepresentedCallableCleanup' || cleanup._tag === 'RepresentedEffectCleanup') return Object.freeze([]) + if (cleanup._tag === 'NominalUnionCleanup') { + if (owner._tag !== 'NominalUnionValue') return Object.freeze([]) + const active = cleanup.variants.find( + (variant) => + variant.ordinal === owner.variantOrdinal && + variant.variant.union.module === owner.variant.union.module && + variant.variant.union.name === owner.variant.union.name && + variant.variant.name === owner.variant.name, + ) + return Object.freeze( + active?.fields.flatMap((field) => { + const value = owner.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.field, field.field), + ) + return value === undefined ? [] : cleanupMembers(field.cleanup, value.value) + }) ?? [], + ) + } if (owner._tag !== 'AggregateValue') return Object.freeze([]) return Object.freeze( cleanup.fields.flatMap((field) => { diff --git a/packages/compiler/src/CleanupPlan.ts b/packages/compiler/src/CleanupPlan.ts index 82e89c66f..c664c4a31 100644 --- a/packages/compiler/src/CleanupPlan.ts +++ b/packages/compiler/src/CleanupPlan.ts @@ -61,6 +61,18 @@ export type CleanupPlan = readonly cleanup: CleanupPlan }> } + | { + readonly _tag: 'NominalUnionCleanup' + readonly type: Type.Nominal + readonly variants: ReadonlyArray<{ + readonly variant: DeclarationFacts.CanonicalUnionVariantId + readonly ordinal: number + readonly fields: ReadonlyArray<{ + readonly field: DeclarationFacts.FieldId + readonly cleanup: CleanupPlan + }> + }> + } | { readonly _tag: 'ArrayCleanup' readonly type: Type.FixedArray @@ -112,6 +124,8 @@ export type CleanupPlan = export const hasHook = (self: CleanupPlan): boolean => self._tag === 'HookCleanup' || (self._tag === 'StructCleanup' && self.fields.some((field) => hasHook(field.cleanup))) || + (self._tag === 'NominalUnionCleanup' && + self.variants.some((variant) => variant.fields.some((field) => hasHook(field.cleanup)))) || (self._tag === 'ArrayCleanup' && hasHook(self.element)) || (self._tag === 'UnionCleanup' && self.cases.some((entry) => hasHook(entry.cleanup))) || ((self._tag === 'CallableCleanup' || self._tag === 'EffectCleanup') && @@ -128,6 +142,8 @@ export const reclaims = (self: CleanupPlan): boolean => self._tag === 'WakeCleanup' || (self._tag === 'HookCleanup' && reclaims(self.inner)) || (self._tag === 'StructCleanup' && self.fields.some((field) => reclaims(field.cleanup))) || + (self._tag === 'NominalUnionCleanup' && + self.variants.some((variant) => variant.fields.some((field) => reclaims(field.cleanup)))) || (self._tag === 'ArrayCleanup' && reclaims(self.element)) || (self._tag === 'UnionCleanup' && self.cases.some((entry) => reclaims(entry.cleanup))) || ((self._tag === 'CallableCleanup' || self._tag === 'EffectCleanup') && @@ -268,7 +284,7 @@ export const cleanupPlan = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') { + if (declaration?._tag !== 'StructDeclaration' && declaration?._tag !== 'UnionDeclaration') { return Object.freeze({ _tag: 'NoCleanup', type }) } const substitution = @@ -277,6 +293,39 @@ export const cleanupPlan = ( type.arguments, ) ?? new Map() const nextSeen = new Set(seen).add(key) + if (declaration._tag === 'UnionDeclaration') { + return Object.freeze({ + _tag: 'NominalUnionCleanup', + type, + variants: Object.freeze( + declaration.variants.flatMap((variant) => + variant.canonical._tag === 'Canonical' + ? [ + Object.freeze({ + variant: variant.canonical.id, + ordinal: variant.id.ordinal, + fields: Object.freeze( + variant.fields.map((field) => + Object.freeze({ + field: field.id, + cleanup: + field.declaredType._tag === 'Resolved' + ? cleanupPlan( + index, + Type.substitute(field.declaredType.type, substitution), + nextSeen, + ) + : Object.freeze({ _tag: 'NoCleanup' as const, type: 'i32' as const }), + }), + ), + ), + }), + ] + : [], + ), + ), + }) + } const structPlan: CleanupPlan = Object.freeze({ _tag: 'StructCleanup', type, @@ -424,6 +473,28 @@ export const specializeCleanup = ( ), ), }) + case 'NominalUnionCleanup': + if (!Type.isNominal(type)) return Object.freeze({ _tag: 'NoCleanup', type }) + return Object.freeze({ + _tag: 'NominalUnionCleanup', + type, + variants: Object.freeze( + cleanup.variants.map((variant) => + Object.freeze({ + variant: variant.variant, + ordinal: variant.ordinal, + fields: Object.freeze( + variant.fields.map((field) => + Object.freeze({ + field: field.field, + cleanup: specializeCleanup(field.cleanup, substitution, resolveConcrete), + }), + ), + ), + }), + ), + ), + }) case 'ArrayCleanup': if (!Type.isFixedArray(type)) return Object.freeze({ _tag: 'NoCleanup', type }) return Object.freeze({ diff --git a/packages/compiler/src/ExecutableOrigin.ts b/packages/compiler/src/ExecutableOrigin.ts index b4a43b6bd..ad20e8f03 100644 --- a/packages/compiler/src/ExecutableOrigin.ts +++ b/packages/compiler/src/ExecutableOrigin.ts @@ -236,6 +236,10 @@ export const make = (operations: Operations) => { ] case 'StructCleanup': return cleanup.fields.flatMap((field) => hookCalls(field.cleanup, index)) + case 'NominalUnionCleanup': + return cleanup.variants.flatMap((variant) => + variant.fields.flatMap((field) => hookCalls(field.cleanup, index)), + ) case 'ArrayCleanup': return hookCalls(cleanup.element, index) case 'UnionCleanup': diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index c98a01e88..2c4dfd1c2 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -382,6 +382,10 @@ const cleanupText = (cleanup: CleanupPlan.CleanupPlan): string => { return `${typeText(cleanup.type)} ${cleanup.fields .map(({ field }) => `#${field.ordinal}`) .join(' → ')}` + case 'NominalUnionCleanup': + return `${typeText(cleanup.type)} active variant · ${cleanup.variants + .map((variant) => `${variant.ordinal}:${variant.variant.name}`) + .join(', ')}` case 'ArrayCleanup': return `${typeText(cleanup.type)} elements in reverse order · ${cleanupText(cleanup.element)}` case 'UnionCleanup': diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index f17f83b6d..50f816b49 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -1,6 +1,6 @@ import type { ControlProvenance } from './Backend.js' import type * as Layout from './Layout.js' -import type * as Match from './Match.js' +import * as Match from './Match.js' import * as Mir from './Mir.js' export type LinearTerminator = @@ -344,13 +344,17 @@ export const expandMatches = ( } const arm = match.arms.find((item) => item.id.ordinal === candidate.ordinal) if (arm === undefined) throw new RangeError('LLVM match expansion lost a candidate arm') + const bindingMember = + arm.member?._tag === 'StructuralTypeMember' && Match.selects(arm.member, member) + ? arm.member + : member const bindings: ReadonlyArray = Object.freeze( arm.bindings.map((binding) => Object.freeze({ _tag: 'BindMatch' as const, scrutinee: match.scrutinee, shape: match.scrutineeShape, - member, + member: bindingMember, binding, provenance: binding.provenance, }), diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index d74be34e7..3934800b6 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -1088,6 +1088,13 @@ const cleanupTypes = (cleanup: CleanupPlan.CleanupPlan): ReadonlyArray cleanupTypes(field.cleanup))] + case 'NominalUnionCleanup': + return [ + cleanup.type, + ...cleanup.variants.flatMap((variant) => + variant.fields.flatMap((field) => cleanupTypes(field.cleanup)), + ), + ] case 'ArrayCleanup': return [cleanup.type, ...cleanupTypes(cleanup.element)] case 'UnionCleanup': @@ -1367,6 +1374,33 @@ const cleanupMatchesSemanticType = ( const key = SilkType.key(type) if (seen.has(key)) return cleanup._tag === 'NoCleanup' const representation = Layout.entry(layout, type)?.representation + if (representation?._tag === 'NominalUnion') { + if ( + cleanup._tag !== 'NominalUnionCleanup' || + cleanup.variants.length !== representation.variants.length + ) + return false + const next = new Set(seen).add(key) + return cleanup.variants.every((variant, ordinal) => { + const expected = representation.variants.at(ordinal) + return ( + expected !== undefined && + variant.ordinal === expected.ordinal && + variant.variant.union.module === expected.variant.union.module && + variant.variant.union.name === expected.variant.union.name && + variant.variant.name === expected.variant.name && + variant.fields.length === expected.fields.length && + variant.fields.every((field, fieldOrdinal) => { + const expectedField = expected.fields.at(fieldOrdinal) + return ( + expectedField !== undefined && + DeclarationFacts.sameFieldId(field.field, expectedField.id) && + cleanupMatchesSemanticType(layout, field.cleanup, expectedField.type, next) + ) + }) + ) + }) + } if (representation?._tag !== 'Aggregate') return cleanup._tag === 'NoCleanup' const requiredHook = representation.cleanupHook if (requiredHook !== undefined) { diff --git a/packages/compiler/src/NativeAggregate.ts b/packages/compiler/src/NativeAggregate.ts index d55548f18..20a86f7dd 100644 --- a/packages/compiler/src/NativeAggregate.ts +++ b/packages/compiler/src/NativeAggregate.ts @@ -14,6 +14,7 @@ import * as Layout from './Layout.js' import * as LayoutVerify from './LayoutVerify.js' import * as LocalSharedControlBlock from './LocalSharedControlBlock.js' import * as LocalSharedPayloadCleanup from './LocalSharedPayloadCleanup.js' +import * as Match from './Match.js' import * as Mir from './Mir.js' import * as NativeArith from './NativeArith.js' import * as NativeCall from './NativeCall.js' @@ -233,6 +234,72 @@ export const dropThroughPlan = Effect.fnUntraced(function* ( } return } + case 'NominalUnionCleanup': { + if ( + plan.variants.every( + (variant) => !variant.fields.some((field) => CleanupPlan.hasEffect(field.cleanup)), + ) + ) + return + const shape = Layout.callingShape(program.layout, plan.type) + const tagValue = values.at(0) + if (shape?.tree._tag !== 'NominalUnionShape' || tagValue === undefined) + throw new RangeError('LLVM nominal union cleanup lost its shape') + for (const variant of plan.variants) { + if (!variant.fields.some((field) => CleanupPlan.hasEffect(field.cleanup))) continue + const matches = yield* FunctionBody.integerCompare( + body, + 'eq', + tagValue, + yield* Constant.integerSigned(builder, i32, BigInt(variant.ordinal)), + `${tag}_v${variant.ordinal}_is`, + ) + const selectedBlock = yield* LlvmBlock.make(body, `${tag}_v${variant.ordinal}_drop`) + const followingBlock = yield* LlvmBlock.make(body, `${tag}_v${variant.ordinal}_next`) + yield* FunctionBody.conditionalBranch(body, matches, selectedBlock, followingBlock) + yield* LlvmBlock.setInsertionPoint(body, selectedBlock) + const identity = Match.nominalUnionVariant( + plan.type, + plan.type, + variant.variant, + variant.ordinal, + ) + for (const [fieldOrdinal, field] of variant.fields.entries()) { + if (!CleanupPlan.hasEffect(field.cleanup)) continue + const physical = Layout.coverageFieldSlots(shape, identity, [field.field]) + const targetLanes = semanticLanesOf(field.cleanup.type) + const fieldValues: Array = [] + for (const [targetOrdinal, ordinal] of physical?.entries() ?? []) { + const value = values.at(ordinal) + const sourceLane = shape.lanes.at(ordinal) + const targetLane = targetLanes.at(targetOrdinal) + if (value === undefined || sourceLane === undefined || targetLane === undefined) + continue + fieldValues.push( + yield* NativeArith.coerceLane( + arith, + value, + sourceLane, + targetLane, + `${tag}_v${variant.ordinal}_f${fieldOrdinal}_${targetOrdinal}_lane`, + ), + ) + } + if (fieldValues.length !== targetLanes.length) + throw new RangeError('LLVM nominal union cleanup lost a field payload lane') + yield* dropThroughPlan( + context, + field.cleanup, + Object.freeze(fieldValues), + `${tag}_v${variant.ordinal}_f${fieldOrdinal}`, + ) + } + yield* FunctionBody.branch(body, followingBlock) + yield* LlvmBlock.setInsertionPoint(body, followingBlock) + yield* NativeStorage.reloadRoots(storage, `${tag}_v${variant.ordinal}_next`) + } + return + } case 'EffectCleanup': for (const slot of plan.slots) { if (!CleanupPlan.hasEffect(slot.cleanup)) continue diff --git a/packages/compiler/src/OwnershipEncoding.ts b/packages/compiler/src/OwnershipEncoding.ts index fec9f84b8..c03d2be17 100644 --- a/packages/compiler/src/OwnershipEncoding.ts +++ b/packages/compiler/src/OwnershipEncoding.ts @@ -69,6 +69,16 @@ const cleanupText = (cleanup: CleanupPlan.CleanupPlan): string => { ) .join(',')}` } + if (cleanup._tag === 'NominalUnionCleanup') { + return `nominal-union:${Type.encode(cleanup.type)} variants=${cleanup.variants + .map( + (variant) => + `${variant.ordinal}:${variant.variant.name}(${variant.fields + .map((field) => `#${field.field.ordinal}(${cleanupText(field.cleanup)})`) + .join(',')})`, + ) + .join(',')}` + } if (cleanup._tag === 'CallableCleanup') { const environment = cleanup.environment._tag === 'CallableEnvironmentSite' diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index c74dec1ea..f0f7f1f7c 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '87e72cb9853c5f259ee26eaa3f0860733f06d998a8cbd4fa7f5695bac0682bba' +export const compilerDigest = '247c20ae6eb0f8f97572f9d7d6eb74c1ff46291b995f0ad1229b643e8fee769c' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index d06ae5d79..2aa4df508 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -891,6 +891,13 @@ const localSharedCleanupDepth = (cleanup: CleanupPlan.CleanupPlan): number => { return localSharedCleanupDepth(cleanup.inner) case 'StructCleanup': return Math.max(0, ...cleanup.fields.map((field) => localSharedCleanupDepth(field.cleanup))) + case 'NominalUnionCleanup': + return Math.max( + 0, + ...cleanup.variants.flatMap((variant) => + variant.fields.map((field) => localSharedCleanupDepth(field.cleanup)), + ), + ) case 'ArrayCleanup': return localSharedCleanupDepth(cleanup.element) case 'UnionCleanup': @@ -944,6 +951,10 @@ const containsExecutionCleanup = (cleanup: CleanupPlan.CleanupPlan): boolean => return containsExecutionCleanup(cleanup.inner) case 'StructCleanup': return cleanup.fields.some((field) => containsExecutionCleanup(field.cleanup)) + case 'NominalUnionCleanup': + return cleanup.variants.some((variant) => + variant.fields.some((field) => containsExecutionCleanup(field.cleanup)), + ) case 'ArrayCleanup': return containsExecutionCleanup(cleanup.element) case 'UnionCleanup': @@ -1784,6 +1795,42 @@ const makeOperationContext = ( ), }) } + case 'NominalUnionCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'NominalUnion') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + plan_.variants.flatMap((variant) => { + const layoutVariant = representation.variants.find( + (candidate) => candidate.ordinal === variant.ordinal, + ) + if (layoutVariant === undefined) return [] + return variant.fields.flatMap((field) => { + if (!CleanupPlan.hasHook(field.cleanup)) return [] + const layoutField = layoutVariant.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), + ) + return layoutField === undefined + ? [] + : [ + Object.freeze({ + cleanup: field.cleanup, + state: byteOffset + representation.payloadOffset + layoutField.offset, + wrap: (instructions: ReadonlyArray) => + Object.freeze([ + ...addressAt(byteOffset), + Instr.memoryAccess('i32.load', memory.memory), + Instr.i32Const(variant.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, instructions, []), + ]), + }), + ] + }) + }), + ), + }) + } case 'ArrayCleanup': { if (!CleanupPlan.hasHook(plan_.element)) return Object.freeze({}) const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation @@ -2225,6 +2272,63 @@ const makeOperationContext = ( ), }) } + case 'NominalUnionCleanup': { + const shape = LayoutPlan.callingShape(plan, plan_.type) + const tag = currentValues.at(0) + if (shape?.tree._tag !== 'NominalUnionShape' || tag === undefined) + throw new RangeError('Wasm nominal union cleanup lost its shape') + return Object.freeze({ + children: Object.freeze( + plan_.variants.flatMap((variant) => { + const identity = Match.nominalUnionVariant( + plan_.type, + plan_.type, + variant.variant, + variant.ordinal, + ) + return variant.fields.flatMap((field) => { + const physical = LayoutPlan.coverageFieldSlots(shape, identity, [field.field]) + const fieldLanes = semanticLanesOf(field.cleanup.type) + if (physical === undefined || physical.length !== fieldLanes.length) return [] + const selected = physical.flatMap((ordinal, index) => { + const value = currentValues.at(ordinal) + const physicalLane = shape.lanes.at(ordinal) + const fieldLane = fieldLanes.at(index) + return value === undefined || + physicalLane === undefined || + fieldLane === undefined + ? [] + : [ + Object.freeze([ + ...value, + ...laneBridge( + laneValueType(plan, physicalLane), + laneValueType(plan, fieldLane), + ), + ]), + ] + }) + if (selected.length !== fieldLanes.length) return [] + return [ + Object.freeze({ + cleanup: field.cleanup, + state: selected, + wrap: (instructions: ReadonlyArray) => + instructions.length === 0 + ? Object.freeze([]) + : Object.freeze([ + ...tag, + Instr.i32Const(variant.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, instructions, []), + ]), + }), + ] + }) + }), + ), + }) + } case 'ArrayCleanup': { const lanes = semanticLanesOf(plan_.type) return Object.freeze({ @@ -2408,6 +2512,42 @@ const makeOperationContext = ( ), }) } + case 'NominalUnionCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'NominalUnion') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + plan_.variants.flatMap((variant) => { + const layoutVariant = representation.variants.find( + (candidate) => candidate.ordinal === variant.ordinal, + ) + if (layoutVariant === undefined) return [] + return variant.fields.flatMap((field) => { + const layoutField = layoutVariant.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), + ) + return layoutField === undefined + ? [] + : [ + Object.freeze({ + cleanup: field.cleanup, + state: currentOffset + representation.payloadOffset + layoutField.offset, + wrap: (instructions: ReadonlyArray) => + instructions.length === 0 + ? Object.freeze([]) + : Object.freeze([ + ...loadAt(address, currentOffset), + Instr.i32Const(variant.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, instructions, []), + ]), + }), + ] + }) + }), + ), + }) + } case 'ArrayCleanup': { const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation if (representation?._tag !== 'Repeated') return Object.freeze({}) @@ -4609,8 +4749,16 @@ const emitMatchOperation = ( if (candidate === undefined) return [Instr.op('unreachable')] const arm = operation.arms.find((entry) => entry.id.ordinal === candidate.ordinal) if (arm === undefined) throw new RangeError('Wasm match lost a candidate arm') + const bindingMember = + arm.member?._tag === 'StructuralTypeMember' && Match.selects(arm.member, member) + ? arm.member + : member const bindings = arm.bindings.flatMap((binding) => { - const physical = LayoutPlan.coverageFieldSlots(operation.scrutineeShape, member, binding.path) + const physical = LayoutPlan.coverageFieldSlots( + operation.scrutineeShape, + bindingMember, + binding.path, + ) if (physical === undefined) { throw new RangeError('Wasm match lost a pattern payload path') } @@ -4661,6 +4809,8 @@ const emitMatchOperation = ( Instr.ifElse(Instr.emptyBlockType, selected, emitDecisions(ordinal + 1)), ] } + if (decision.member._tag !== 'NominalUnionVariant' && operation.scrutineeType._tag !== 'Union') + return selected const tag = slots(operation.scrutinee).at(0) if (tag === undefined) throw new RangeError('Wasm match has no tag lane') if (decision.member._tag === 'NominalUnionVariant') { @@ -4687,7 +4837,6 @@ const emitMatchOperation = ( Instr.ifElse(Instr.emptyBlockType, selected, emitDecisions(ordinal + 1)), ] } - if (operation.scrutineeType._tag !== 'Union') return selected const outer = operation.scrutineeShape.tree._tag === 'SumShape' ? operation.scrutineeShape.tree.members.find((candidate) => diff --git a/packages/compiler/test/BoxHeapIndirection.test.ts b/packages/compiler/test/BoxHeapIndirection.test.ts index 5e620fb8e..072aa47f3 100644 --- a/packages/compiler/test/BoxHeapIndirection.test.ts +++ b/packages/compiler/test/BoxHeapIndirection.test.ts @@ -2,6 +2,7 @@ import { assert, it } from '@effect/vitest' import * as Effect from 'effect/Effect' import * as Analysis from '../src/Analysis.js' import type * as CleanupPlan from '../src/CleanupPlan.js' +import * as MirVerification from '../src/MirVerification.js' import * as ModuleClosure from '../src/ModuleClosure.js' import * as NameResolution from '../src/NameResolution.js' import * as SourceFile from '../src/SourceFile.js' @@ -136,6 +137,45 @@ it.effect( 120_000, ) +it.effect('releases only the active nominal union variant payload', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'box-heap-indirection/nominal-union', + ascii(`import silk.allocator { OutOfMemoryError, Allocator, SystemAllocator } +import silk.effect as Effect +import silk.box { Box, make as boxMake } + +union Owner { Empty, Full { boxed: Box } } + +effect fn useOwner() -> i32 ! OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + let boxed = run boxMake(42) |> Effect.provideMut(&mut allocator) + let owner = Owner.Full { boxed: move boxed } + drop owner + return 42 +} + +effect fn recover(error: OutOfMemoryError) -> i32 { return 0 } +pub fn main() -> i32 { return run Effect.catchAll(useOwner(), recover) }`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(snapshot), []) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(snapshot)), []) + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual(evaluated._tag, 'Completed') + if (evaluated._tag !== 'Completed') return + assert.strictEqual(evaluated.result.value, 42n) + assert.deepEqual(counts(Projections.allocationTraceEventsOf(evaluated)), { + acquires: 1, + releases: 1, + }) + const wasm = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + /** * The trace above only means something if the count it asserts is the count the boxed values * actually cost. The same tree with the boxes counted by hand: seven nodes, six of them boxed. diff --git a/packages/compiler/test/Ownership.test.ts b/packages/compiler/test/Ownership.test.ts index 687d04949..222e90843 100644 --- a/packages/compiler/test/Ownership.test.ts +++ b/packages/compiler/test/Ownership.test.ts @@ -881,6 +881,30 @@ pub fn main() -> i32 { return 0 }`, }) }) +it('plans cleanup only through the active nominal union variant', () => { + const facts = check( + 'ownership://nominal-union-cleanup.silk', + `union MaybeAllocation { None, Some { value: Allocation } } +fn consume(value: MaybeAllocation) -> i32 { return 42 } +pub fn main() -> i32 { return 0 }`, + ) + const cleanup = facts.functions.at(0)?.exits.at(0)?.releases.at(0)?.cleanup + + assert.deepEqual(facts.diagnostics, []) + assert.strictEqual(cleanup?._tag, 'NominalUnionCleanup') + if (cleanup?._tag !== 'NominalUnionCleanup') return + assert.deepEqual( + cleanup.variants.map((variant) => ({ + name: variant.variant.name, + fields: variant.fields.map((field) => field.cleanup._tag), + })), + [ + { name: 'None', fields: [] }, + { name: 'Some', fields: ['AllocationCleanup'] }, + ], + ) +}) + it('ends exclusive service access when a provided operation returns', () => { const facts = check( 'ownership://service-provider-loan.silk', diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 059ab129f..4e8c0717f 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -288,6 +288,143 @@ pub fn main() -> i32 { return unwrap(Option.Some { value: 42 }) }`), }), ) +it.effect('keeps nominal variants nested beneath structural union roots', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/hierarchical-match', + ascii(`union HttpError { Timeout, DNS { code: i32 } } +struct OutOfMemoryError {} + +fn inspect(error: HttpError | OutOfMemoryError) -> i32 { + return match move error { + HttpError.DNS { code } => code + HttpError.Timeout => 1 + OutOfMemoryError other => 0 + } +} + +fn classify(error: HttpError | OutOfMemoryError) -> i32 { + return match move error { + HttpError whole => 7 + OutOfMemoryError other => 0 + } +} + +pub fn main() -> i32 { + return inspect(HttpError.DNS { code: 42 }) + classify(HttpError.Timeout) +}`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + const returned = Analysis.rootAnalysis(self).functions.at(0)?.returnedExpression + assert.strictEqual(returned?._tag, 'Match') + if (returned?._tag !== 'Match') return + assert.deepEqual(returned.members.map(Match.encodeIdentity), [ + 'union-values/hierarchical-match.HttpError::union-values/hierarchical-match.HttpError.Timeout', + 'union-values/hierarchical-match.HttpError::union-values/hierarchical-match.HttpError.DNS', + 'union-values/hierarchical-match.OutOfMemoryError', + ]) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(self)), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 49n) + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 49) + }), +) + +it.effect('keeps generic applications and never-payload variants as distinct coverage leaves', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/generic-coverage', + ascii(`union Option { Some { value: T }, None } +union Result { Success { value: T }, Failure { error: E } } + +fn read(option: Option | Option) -> i32 { + return match move option { + Option.Some { value } => value + Option.None => 0 + Option.Some { value } => 1 + Option.None => 0 + } +} + +fn required(result: Result) -> i32 { + return match move result { + Result.Success { value } => value + Result.Failure { error } => 0 + } +} + +pub fn main() -> i32 { return read(Option.Some { value: 42 }) }`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + const functions = Analysis.rootAnalysis(self).functions + const read = functions.at(0)?.returnedExpression + const required = functions.at(1)?.returnedExpression + assert.strictEqual(read?._tag, 'Match') + assert.strictEqual(required?._tag, 'Match') + if (read?._tag !== 'Match' || required?._tag !== 'Match') return + assert.strictEqual(read.members.length, 4) + assert.deepEqual( + read.members.map((member) => Type.encode(Match.sourceType(member))), + [ + 'union-values/generic-coverage.Option', + 'union-values/generic-coverage.Option', + 'union-values/generic-coverage.Option', + 'union-values/generic-coverage.Option', + ], + ) + assert.deepEqual(required.members.map(Match.encodeIdentity), [ + 'union-values/generic-coverage.Result::union-values/generic-coverage.Result.Success', + 'union-values/generic-coverage.Result::union-values/generic-coverage.Result.Failure', + ]) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(self)), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + +it.effect('keeps affine variant payloads available after a false guard', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/guarded-affine', + ascii(`struct Token { value: i32 } +union Option { Some { value: T }, None } + +fn select(option: Option, guard: bool) -> i32 { + return match move option { + Option.Some { value } if guard => value.value + Option.Some { value } => value.value + 1 + Option.None => 0 + } +} + +pub fn main() -> i32 { + return select(Option.Some { value: Token { value: 41 } }, false) +}`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(self)), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + it.effect('evaluates initializers in source order before constructing in declaration order', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( From ec858fdaf764744158e93a771f0ca83b8c857abb Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 14:22:51 -0300 Subject: [PATCH 10/42] feat(compiler): support nominal union conformances --- openspec/changes/add-nominal-unions/tasks.md | 4 +- packages/compiler/src/CleanupPlan.ts | 52 ++++++++++--------- packages/compiler/src/ConformanceProof.ts | 19 +++++-- .../src/ToolchainIntegrity.generated.ts | 2 +- .../compiler/test/DeclarationIndex.test.ts | 30 +++++++++++ .../compiler/test/OperatorContracts.test.ts | 24 +++++++++ packages/compiler/test/Ownership.test.ts | 27 ++++++++++ 7 files changed, 127 insertions(+), 31 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 73a82b2f8..ff5dcf04c 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -23,7 +23,7 @@ - [x] 3.4 Implement unit and named-field construction with complete field initialization, construction authority, visibility fences, type compatibility, represented fields, and precise parent result types, and verify cross-module private fields block raw construction. - [x] 3.5 Preserve every variant through generic specialization, including equal and `never` payloads while independently renormalizing structural-union fields, and verify specialization facts never collapse or flatten variants. - [x] 3.6 Reject direct parent-union field projection and common-field synthesis while retaining diagnostic facts, and verify `result.value` is unavailable until a variant pattern binds its payload. -- [ ] 3.7 Admit interface, operator, Copy, and Drop declarations against nominal union parents through the ordinary conformance/coherence path, and verify variant names do not become lookup or implementation targets. +- [x] 3.7 Admit interface, operator, Copy, and Drop declarations against nominal union parents through the ordinary conformance/coherence path, and verify variant names do not become lookup or implementation targets. ## 4. Variant Patterns and Hierarchical Coverage @@ -36,7 +36,7 @@ ## 5. Ownership, Represented Fields, and Cleanup -- [ ] 5.1 Apply affine-by-default ownership and explicit Copy validation across every specialized variant field, and verify all-Copy payloads remain affine without `impl Copy` while one affine field rejects the implementation. +- [x] 5.1 Apply affine-by-default ownership and explicit Copy validation across every specialized variant field, and verify all-Copy payloads remain affine without `impl Copy` while one affine field rejects the implementation. - [ ] 5.2 Build active-variant cleanup plans that reuse nominal Drop ordering and clean only initialized fields of the selected variant, and verify success, typed-failure, and ordinary scope exits release each owned payload exactly once. - [ ] 5.3 Implement moved and borrowed variant-pattern ownership, including branch-local cleanup of omitted fields and rejection of invalid partial moves, and verify extracted and omitted fields have one final owner. - [ ] 5.4 Realize callable-bounded fields only inside the active variant using exact static callable storage and access rules, and verify unsupported representations retain the pre-MIR storage fence. diff --git a/packages/compiler/src/CleanupPlan.ts b/packages/compiler/src/CleanupPlan.ts index c664c4a31..433efd021 100644 --- a/packages/compiler/src/CleanupPlan.ts +++ b/packages/compiler/src/CleanupPlan.ts @@ -293,8 +293,33 @@ export const cleanupPlan = ( type.arguments, ) ?? new Map() const nextSeen = new Set(seen).add(key) - if (declaration._tag === 'UnionDeclaration') { + const withDropHook = (inner: CleanupPlan): CleanupPlan => { + const witness = ConformanceProof.witness(index, type, Type.dropCapability) + if (witness?._tag !== 'SourceConformanceWitness') return inner + const conformance = index.modules + .find((module) => module.module === witness.module) + ?.conformances.find((candidate) => candidate.ordinal === witness.ordinal) + if (conformance?.provider._tag !== 'Resolved') return inner + const inferred = new Map() + if (!TypeInference.infer(conformance.provider.type, type, inferred)) return inner return Object.freeze({ + _tag: 'HookCleanup', + type, + hook: Object.freeze({ + _tag: 'CanonicalDeclarationId' as const, + module: witness.module, + name: `drop@impl#${witness.ordinal}`, + }), + typeArguments: Object.freeze( + conformance.typeParameters.map( + (parameter) => inferred.get(Type.key(parameter.type)) ?? parameter.type, + ), + ), + inner, + }) + } + if (declaration._tag === 'UnionDeclaration') { + const unionPlan: CleanupPlan = Object.freeze({ _tag: 'NominalUnionCleanup', type, variants: Object.freeze( @@ -325,6 +350,7 @@ export const cleanupPlan = ( ), ), }) + return withDropHook(unionPlan) } const structPlan: CleanupPlan = Object.freeze({ _tag: 'StructCleanup', @@ -342,29 +368,7 @@ export const cleanupPlan = ( ), }) // A source Drop conformance runs its hook before automatic field cleanup. - const witness = ConformanceProof.witness(index, type, Type.dropCapability) - if (witness?._tag !== 'SourceConformanceWitness') return structPlan - const conformance = index.modules - .find((module) => module.module === witness.module) - ?.conformances.find((candidate) => candidate.ordinal === witness.ordinal) - if (conformance?.provider._tag !== 'Resolved') return structPlan - const inferred = new Map() - if (!TypeInference.infer(conformance.provider.type, type, inferred)) return structPlan - return Object.freeze({ - _tag: 'HookCleanup', - type, - hook: Object.freeze({ - _tag: 'CanonicalDeclarationId' as const, - module: witness.module, - name: `drop@impl#${witness.ordinal}`, - }), - typeArguments: Object.freeze( - conformance.typeParameters.map( - (parameter) => inferred.get(Type.key(parameter.type)) ?? parameter.type, - ), - ), - inner: structPlan, - }) + return withDropHook(structPlan) } export const cleanupTypeAtPath = ( diff --git a/packages/compiler/src/ConformanceProof.ts b/packages/compiler/src/ConformanceProof.ts index 9ccea386f..0316f5fe3 100644 --- a/packages/compiler/src/ConformanceProof.ts +++ b/packages/compiler/src/ConformanceProof.ts @@ -6,6 +6,8 @@ import type { ConformanceFact, ConformanceWitness, ContractFact, + FieldFact, + UnionVariantFact, } from './DeclarationFacts.js' import { byCanonical } from './DeclarationFacts.js' import type { Index } from './DeclarationIndex.js' @@ -284,8 +286,8 @@ export const copyProof = ( _tag: 'NotCopy', reason: `${Type.encode(type)} also implements Drop`, }) - if (declaration?._tag !== 'StructDeclaration') - return Object.freeze({ _tag: 'NotCopy', reason: `${Type.encode(type)} is not a struct` }) + if (declaration?._tag !== 'StructDeclaration' && declaration?._tag !== 'UnionDeclaration') + return Object.freeze({ _tag: 'NotCopy', reason: `${Type.encode(type)} is not an aggregate` }) if (declaration.dependency._tag === 'Unavailable') return Object.freeze({ _tag: 'UnavailableCopy', @@ -298,7 +300,16 @@ export const copyProof = ( ) ?? new Map() const nestedAssumptions = new Set([...assumptions, ...copyAssumptions(selected.conformance)]) const nestedActive = new Set(active).add(key) - for (const field of declaration.fields) { + const fields: ReadonlyArray<{ + readonly field: FieldFact + readonly variant?: UnionVariantFact + }> = + declaration._tag === 'StructDeclaration' + ? declaration.fields.map((field) => Object.freeze({ field })) + : declaration.variants.flatMap((variant) => + variant.fields.map((field) => Object.freeze({ field, variant })), + ) + for (const { field, variant } of fields) { if (field.declaredType._tag !== 'Resolved') return Object.freeze({ _tag: 'UnavailableCopy', @@ -309,7 +320,7 @@ export const copyProof = ( if (proof._tag !== 'Copy') return Object.freeze({ ...proof, - reason: `field ${field.name._tag === 'Present' ? field.name.spelling : `#${field.id.ordinal}`} (${Type.encode(fieldType)}): ${proof.reason}`, + reason: `${variant === undefined ? '' : `variant ${variant.name._tag === 'Present' ? variant.name.spelling : `#${variant.id.ordinal}`} `}field ${field.name._tag === 'Present' ? field.name.spelling : `#${field.id.ordinal}`} (${Type.encode(fieldType)}): ${proof.reason}`, }) } return provedCopy diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index f0f7f1f7c..32025a2f9 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '247c20ae6eb0f8f97572f9d7d6eb74c1ff46291b995f0ad1229b643e8fee769c' +export const compilerDigest = '76dde9bb4b040c3b6836570b9ddb381b8d934c61d02d89ef92349e660c0fe6e2' diff --git a/packages/compiler/test/DeclarationIndex.test.ts b/packages/compiler/test/DeclarationIndex.test.ts index 7e028980e..abb9fd41e 100644 --- a/packages/compiler/test/DeclarationIndex.test.ts +++ b/packages/compiler/test/DeclarationIndex.test.ts @@ -1432,6 +1432,36 @@ impl Clock for FixedClock {}`, }), ) +it.effect('validates Copy over every specialized nominal union variant field', () => + Effect.gen(function* () { + const index = yield* collect('union-copy', [ + [ + 'union-copy', + `union Choice { Empty, Present { value: T } } +impl Copy for Choice {} +union Implicit { Left { value: i32 }, Right } +union Owned { Empty, Present { allocation: Allocation } } +impl Copy for Owned {}`, + ], + ]) + const choice = (element: Type.Type): Type.Nominal => + Type.nominal('union-copy', 'Choice', [element]) + + assert.isTrue(ConformanceProof.copyType(index, choice('i32'))) + assert.isFalse(ConformanceProof.copyType(index, choice(Type.nominal('union-copy', 'Implicit')))) + assert.isFalse(ConformanceProof.copyType(index, Type.nominal('union-copy', 'Implicit'))) + assert.isFalse(ConformanceProof.copyType(index, Type.nominal('union-copy', 'Owned'))) + assert.deepEqual( + index.modules.at(0)?.conformances.map((conformance) => conformance.validity._tag), + ['ValidConformance', 'InvalidConformance'], + ) + assert.deepEqual( + index.diagnostics.map((diagnostic) => diagnostic.code), + ['SEM0083'], + ) + }), +) + it.effect('indexes parametric conformances with bound parameters', () => Effect.gen(function* () { const index = yield* collect('parametric', [ diff --git a/packages/compiler/test/OperatorContracts.test.ts b/packages/compiler/test/OperatorContracts.test.ts index 341d5f7be..5e4a8eeb9 100644 --- a/packages/compiler/test/OperatorContracts.test.ts +++ b/packages/compiler/test/OperatorContracts.test.ts @@ -50,6 +50,30 @@ pub fn main() -> i32 { }), ) +it.effect('uses nominal union parents as ordinary interface and operator providers', () => + Effect.gen(function* () { + const self = yield* snapshot(`union Choice { Left { value: i32 }, Right { value: i32 } } + +interface Merge { operator + fn add(left: Self, right: Self) -> Self } + +fn add(left: Choice, right: Choice) -> Choice { return move left } +impl Merge for Choice { add: Choice.add } + +pub fn main() -> i32 { + let combined = Choice.Left { value: 42 } + Choice.Right { value: 0 } + return match move combined { + Choice.Left { value } => value + Choice.Right { value } => value + } +}`) + + assert.deepEqual(Analysis.diagnostics(self), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + }), +) + it.effect('selects a marked operation through an ordinary generic bound', () => Effect.gen(function* () { const self = yield* snapshot(`${vectorContracts} diff --git a/packages/compiler/test/Ownership.test.ts b/packages/compiler/test/Ownership.test.ts index 222e90843..14b753395 100644 --- a/packages/compiler/test/Ownership.test.ts +++ b/packages/compiler/test/Ownership.test.ts @@ -905,6 +905,33 @@ pub fn main() -> i32 { return 0 }`, ) }) +it('admits Copy and Drop conformances on nominal union parents', () => { + const copied = check( + 'ownership://nominal-union-copy.silk', + `union Choice { First { value: i32 }, Second } +impl Copy for Choice {} +fn duplicate(value: Choice) -> Choice { let copy = value return move copy } +pub fn main() -> i32 { return 0 }`, + ) + const dropped = check( + 'ownership://nominal-union-drop.silk', + `union Owner { Empty, Present { value: i32 } } +impl Drop for Owner { + fn drop(self: &mut Owner) -> () { return () } +} +fn consume(value: Owner) -> i32 { return 0 } +pub fn main() -> i32 { return 0 }`, + ) + + assert.deepEqual(copied.diagnostics, []) + assert.strictEqual(copied.functions.at(0)?.bindings.at(0)?.category._tag, 'Copyable') + assert.deepEqual(dropped.diagnostics, []) + assert.strictEqual( + dropped.functions.at(0)?.exits.at(0)?.releases.at(0)?.cleanup._tag, + 'HookCleanup', + ) +}) + it('ends exclusive service access when a provided operation returns', () => { const facts = check( 'ownership://service-provider-loan.silk', From da7468c3e491ffcb8839e16f290e0467791d6b69 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 15:06:11 -0300 Subject: [PATCH 11/42] feat: make checked outcomes carrier-neutral --- packages/compiler/src/BootstrapEvaluation.ts | 72 +++++---- packages/compiler/src/CallResolution.ts | 36 ++++- .../compiler/src/DeclarationResolution.ts | 10 +- packages/compiler/src/Elaboration.ts | 1 + packages/compiler/src/ExecutableOrigin.ts | 22 ++- packages/compiler/src/ExpressionAnalysis.ts | 4 +- packages/compiler/src/Hir.ts | 3 +- packages/compiler/src/HirLowering.ts | 1 + packages/compiler/src/Intrinsic.ts | 38 ++++- packages/compiler/src/Layout.ts | 2 + packages/compiler/src/LowerBuiltin.ts | 43 ++++-- packages/compiler/src/LowerExpression.ts | 31 ++-- packages/compiler/src/Mir.ts | 11 +- packages/compiler/src/MirLinearization.ts | 105 ++++++++++++- packages/compiler/src/MirVerification.ts | 39 +++-- packages/compiler/src/NativeOperation.ts | 2 +- .../compiler/src/NativeScalarOperation.ts | 35 +---- packages/compiler/src/Stdlib.generated.ts | 124 +++++++-------- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/ValueType.ts | 11 +- packages/compiler/src/WasmBackend.ts | 143 +++++++++++------- packages/compiler/stdlib/silk/char.silk | 4 +- .../compiler/stdlib/silk/child_process.silk | 6 +- packages/compiler/stdlib/silk/effect.silk | 62 +++----- packages/compiler/stdlib/silk/filesystem.silk | 36 ++--- packages/compiler/stdlib/silk/format.silk | 84 +++++----- packages/compiler/stdlib/silk/hash_map.silk | 24 +-- packages/compiler/stdlib/silk/hash_set.silk | 16 +- packages/compiler/stdlib/silk/host_input.silk | 6 +- packages/compiler/stdlib/silk/i16.silk | 32 ++-- packages/compiler/stdlib/silk/i32.silk | 32 ++-- packages/compiler/stdlib/silk/i64.silk | 32 ++-- packages/compiler/stdlib/silk/i8.silk | 32 ++-- packages/compiler/stdlib/silk/isize.silk | 32 ++-- packages/compiler/stdlib/silk/layout.silk | 7 +- .../compiler/stdlib/silk/local_scheduler.silk | 33 ++-- packages/compiler/stdlib/silk/logger.silk | 8 +- packages/compiler/stdlib/silk/option.silk | 43 +++--- .../stdlib/silk/os_child_process.silk | 6 +- .../compiler/stdlib/silk/os_filesystem.silk | 86 +++++------ .../compiler/stdlib/silk/os_host_input.silk | 10 +- .../stdlib/silk/os_monotonic_clock.silk | 6 +- .../stdlib/silk/os_standard_input.silk | 6 +- packages/compiler/stdlib/silk/result.silk | 78 +++------- packages/compiler/stdlib/silk/string.silk | 19 ++- packages/compiler/stdlib/silk/u16.silk | 32 ++-- packages/compiler/stdlib/silk/u32.silk | 32 ++-- packages/compiler/stdlib/silk/u64.silk | 32 ++-- packages/compiler/stdlib/silk/u8.silk | 32 ++-- packages/compiler/stdlib/silk/unicode.silk | 8 +- packages/compiler/stdlib/silk/usize.silk | 32 ++-- packages/compiler/stdlib/silk/vector.silk | 10 +- packages/compiler/test/IntegerScalars.test.ts | 90 +++++++++-- 53 files changed, 949 insertions(+), 754 deletions(-) diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index e5c6987cc..3e5dc1774 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -3929,38 +3929,48 @@ function* executeFunction( ) return arithmetic >= range.minimum && arithmetic <= range.maximum })()) - const member = success ? operation.success : operation.failure - const entry = program.layout.entries.find((candidate) => - Type.equals(candidate.type, member), - ) - if (entry?._tag !== 'LayoutEntry' || entry.representation._tag !== 'Aggregate') - throw new RangeError('Target plan omitted a canonical Option member') - const payload: AggregateValue = Object.freeze({ - _tag: 'AggregateValue', - type: member, - fields: Object.freeze( - success - ? entry.representation.fields.map((field) => - Object.freeze({ - field: field.id, - value: - target.category === 'Character' - ? characterValue(Number(arithmetic)) - : integerValue(target.spelling, arithmetic), - }), - ) - : [], - ), - }) - write(operation.destination, { - value: Object.freeze({ - _tag: 'UnionValue', - type: operation.type.type, - member, - payload, + if (success && arithmetic !== undefined) { + write(operation.value, { + value: + target.category === 'Character' + ? characterValue(Number(arithmetic)) + : integerValue(target.spelling, arithmetic), + fromCall: false, + }) + } + const callable = success ? operation.present : operation.absent + const unused = success ? operation.absent : operation.present + const cleanup = success ? operation.absentCleanup : operation.presentCleanup + const callableType = fn.localTypes.at(callable.ordinal) + if (callableType?._tag !== 'CallableValue') + throw new RangeError('MIR checked scalar operation lost its carrier callable') + const carrier = yield* executeOperations([ + Object.freeze({ + _tag: 'Drop' as const, + local: unused, + cleanup, + provenance: operation.provenance, }), - fromCall: false, - }) + Object.freeze({ + _tag: 'ApplyCallable' as const, + destination: operation.destination, + callable, + typeArguments: + callableType.environment?.callable.typeArguments ?? + callableType.storage?.realization.targetArguments ?? + callableType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: success ? Object.freeze([operation.value]) : Object.freeze([]), + callableType: callableType.type, + access: callableType.type.mode, + evaluation: 'CalleeThenArguments' as const, + realization: 'Environment' as const, + type: operation.type, + provenance: operation.provenance, + }), + ]) + if (carrier !== undefined) return carrier break } case 'Construct': { diff --git a/packages/compiler/src/CallResolution.ts b/packages/compiler/src/CallResolution.ts index 2a39e581d..fd273c3c9 100644 --- a/packages/compiler/src/CallResolution.ts +++ b/packages/compiler/src/CallResolution.ts @@ -1747,6 +1747,7 @@ export const analyzeFunctionItem = ( node: SyntaxTree.Node, declarations: ReadonlyArray, resolution: ResolutionContext, + expected?: SemanticType, ): ExpressionResult | undefined => { const reference = resolvedFunctionReference(source, node, declarations, resolution) if (reference === undefined) { @@ -1787,6 +1788,7 @@ export const analyzeFunctionItem = ( _tag: 'FunctionItem', reference: missing, path: referencePath(node), + typeArguments: Object.freeze([]), type: unavailableExpressionType, syntax: node, }), @@ -1794,7 +1796,38 @@ export const analyzeFunctionItem = ( type: undefined, }) } - const callable = callableTypeOfReference(reference) + const unresolvedCallable = callableTypeOfReference(reference) + const contract = resolvedCallableContract(reference) + const contextual = new Map() + const expectedCallable = + expected !== undefined && Type.isCallable(expected) ? expected : undefined + const contextualPattern = + unresolvedCallable === undefined || expectedCallable === undefined + ? undefined + : Type.callable( + unresolvedCallable.parameters, + unresolvedCallable.result, + expectedCallable.mode, + unresolvedCallable.schema, + unresolvedCallable.unsafe, + ) + const specialized = + contextualPattern !== undefined && + expectedCallable !== undefined && + TypeInference.infer(contextualPattern, expectedCallable, contextual) + let callable = unresolvedCallable + if (callable !== undefined && specialized) { + const contextualCallable = Type.substitute(callable, contextual) + callable = Type.isCallable(contextualCallable) ? contextualCallable : undefined + } + const typeArguments = Object.freeze( + specialized + ? (contract?.binders ?? []).flatMap((parameter) => { + const argument = contextual.get(Type.key(parameter)) + return argument === undefined ? [] : [argument] + }) + : [], + ) const type = callable === undefined ? unavailableExpressionType : availableExpressionType(callable) return Object.freeze({ @@ -1802,6 +1835,7 @@ export const analyzeFunctionItem = ( _tag: 'FunctionItem', reference, path: referencePath(node), + typeArguments, type, syntax: node, }), diff --git a/packages/compiler/src/DeclarationResolution.ts b/packages/compiler/src/DeclarationResolution.ts index b7bb8244d..7b97c2124 100644 --- a/packages/compiler/src/DeclarationResolution.ts +++ b/packages/compiler/src/DeclarationResolution.ts @@ -612,15 +612,7 @@ export const resolveDeclaredType = ( diagnostics: Object.freeze(diagnostics), }) } - const firstConcrete = concrete.at(0) - const type = - target.fact.type.module === 'silk/option' && - target.fact.type.name === 'Option' && - concrete.length === 1 && - firstConcrete !== undefined && - Type.isTypeArgument(firstConcrete) - ? Type.option(firstConcrete) - : Type.specializeNominal(target.fact.type, concrete) + const type = Type.specializeNominal(target.fact.type, concrete) return Object.freeze({ fact: Object.freeze({ _tag: 'Resolved', diff --git a/packages/compiler/src/Elaboration.ts b/packages/compiler/src/Elaboration.ts index 59fbac7ba..4643bcc55 100644 --- a/packages/compiler/src/Elaboration.ts +++ b/packages/compiler/src/Elaboration.ts @@ -765,6 +765,7 @@ export interface FunctionItemExpressionFact { readonly _tag: 'FunctionItem' readonly reference: CallReferenceFact readonly path: ReferencePathFact + readonly typeArguments: ReadonlyArray readonly type: ExpressionTypeFact readonly syntax: SyntaxTree.Node } diff --git a/packages/compiler/src/ExecutableOrigin.ts b/packages/compiler/src/ExecutableOrigin.ts index ad20e8f03..4e4fa2f7f 100644 --- a/packages/compiler/src/ExecutableOrigin.ts +++ b/packages/compiler/src/ExecutableOrigin.ts @@ -696,11 +696,14 @@ export const make = (operations: Operations) => { ): Type.CallableIdentityArgument | undefined => { if (expression._tag === 'FunctionItem') { const target = Hir.callableTargetIdentity(expression.target) + const typeArguments = expression.typeArguments.map((argument) => + Type.substituteGenericArgument(argument, context.substitution), + ) const identity = target._tag === 'Declaration' ? `declaration:${target.module}:${target.name}` : `builtin:${target.actor}:${target.operation}` - return Type.callableIdentityArgument(identity, target) + return Type.callableIdentityArgument(identity, target, typeArguments) } if (expression._tag === 'CallableSection') { const typeArguments = expression.typeArguments.map((argument) => @@ -1739,10 +1742,21 @@ export const make = (operations: Operations) => { continue const declaration = declarationTarget(value.target) if (declaration === undefined) continue - const substitution = value._tag === 'CallableSection' ? value.substitution : new Map() - const arguments_ = targetArguments(value.target, substitution, results) const target = targetFunction(results, declaration) - if (arguments_ === undefined || target === undefined) continue + if (target === undefined) continue + const substitution = value._tag === 'CallableSection' ? value.substitution : new Map() + let arguments_: ReadonlyArray | undefined + if (value._tag === 'FunctionItem') { + if (value.typeArguments.length === target.declaration.typeParameters.length) + arguments_ = Object.freeze( + value.typeArguments.map((argument) => + Type.substituteGenericArgument(argument, ownerSubstitution), + ), + ) + } else { + arguments_ = targetArguments(value.target, substitution, results) + } + if (arguments_ === undefined) continue const targetSubstitution = TypeInference.substitution( target.declaration.typeParameters.map((parameter) => parameter.type), arguments_, diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index 0e29bcdfd..ce8230143 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -5640,7 +5640,7 @@ export function analyzeExpression( return value return ( analyzeConstantReference(source, node, resolution) ?? - analyzeFunctionItem(source, node, declarations, resolution) ?? + analyzeFunctionItem(source, node, declarations, resolution, expected) ?? value ) } @@ -5805,7 +5805,7 @@ export function analyzeExpression( return ( analyzeEnumMember(source, node, resolution, expected) ?? analyzeConstantReference(source, node, resolution) ?? - analyzeFunctionItem(source, node, declarations, resolution) ?? + analyzeFunctionItem(source, node, declarations, resolution, expected) ?? analyzeProjection(source, node, declarations, declaration, scope, resolution) ) } diff --git a/packages/compiler/src/Hir.ts b/packages/compiler/src/Hir.ts index 14ceab156..11b226462 100644 --- a/packages/compiler/src/Hir.ts +++ b/packages/compiler/src/Hir.ts @@ -657,6 +657,7 @@ export type Expression = | { readonly _tag: 'FunctionItem' readonly target: CallableTarget + readonly typeArguments: ReadonlyArray readonly type: Type.Callable readonly span: SourceSpan.SourceSpan } @@ -1747,7 +1748,7 @@ const encodeExpression = (expression: Expression, depth: number): string => { expression.target._tag === 'DeclarationCallableTarget' ? `${expression.target.declaration.module}.${expression.target.declaration.name}` : `${expression.target.actor}.${expression.target.operation}` - } : ${Type.encode(expression.type)} ${spanText(expression.span)}` + }<${expression.typeArguments.map(Type.genericArgumentKey).join(',')}> : ${Type.encode(expression.type)} ${spanText(expression.span)}` case 'CallableSection': return [ `${indent}callable-section site=${executableSiteLabel(expression.site)} mode=${expression.mode.toLowerCase()} remaining=${expression.remainingParameters.map((ordinal) => `p${ordinal}`).join(',')} target=${ diff --git a/packages/compiler/src/HirLowering.ts b/packages/compiler/src/HirLowering.ts index c6b223f60..09c7adf08 100644 --- a/packages/compiler/src/HirLowering.ts +++ b/packages/compiler/src/HirLowering.ts @@ -1021,6 +1021,7 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. return Object.freeze({ _tag: 'FunctionItem', target, + typeArguments: fact.typeArguments, type: fact.type.type, span: fact.syntax.span, }) diff --git a/packages/compiler/src/Intrinsic.ts b/packages/compiler/src/Intrinsic.ts index 84db4608d..90687722e 100644 --- a/packages/compiler/src/Intrinsic.ts +++ b/packages/compiler/src/Intrinsic.ts @@ -668,8 +668,13 @@ const scalarOperation = (scalar: Scalar.Scalar, operation: Scalar.Operation): Op break } const checked = operation.result === 'OptionSelf' || operation.result === 'OptionTarget' - const result = checked ? `Option<${concreteResult}>` : concreteResult - const semanticResult = checked ? Type.option(concreteResult) : concreteResult + const carrierOwner = Object.freeze({ + module: 'Intrinsic', + name: `$${scalar.spelling}.${operation.spelling}`, + }) + const carrierResult = Type.parameter(carrierOwner, 0, 'R') + const result = checked ? 'R' : concreteResult + const semanticResult = checked ? carrierResult : concreteResult const parameterNames = operation.arity === 1 ? Object.freeze(['value']) : Object.freeze(['left', 'right']) const semanticParameters = @@ -678,18 +683,37 @@ const scalarOperation = (scalar: Scalar.Scalar, operation: Scalar.Operation): Op const contractParameters = borrowed ? Object.freeze(semanticParameters.map((type) => Type.reference('Shared', type))) : semanticParameters + const carrierParameters = checked + ? Object.freeze([ + valueParameter('present', `once fn(${concreteResult}) -> R`), + valueParameter('absent', 'once fn() -> R'), + ]) + : Object.freeze([]) + const semanticCarrierParameters = checked + ? Object.freeze([ + Type.callable(Object.freeze([concreteResult]), carrierResult, 'Take'), + Type.callable(Object.freeze([]), carrierResult, 'Take'), + ]) + : Object.freeze([]) return builtin({ actor: scalar.spelling, name: operation.spelling, operation: operation.code, - parameters: Object.freeze( - parameterNames.map((name, ordinal) => { + ...(checked + ? { + typeParameters: Object.freeze(['R']), + semanticTypeParameters: Object.freeze([carrierResult]), + } + : {}), + parameters: Object.freeze([ + ...parameterNames.map((name, ordinal) => { const type = semanticParameters.at(ordinal) ?? scalar.spelling return valueParameter(name, borrowed ? `&${type}` : type) }), - ), - semanticParameters, - callParameters: contractParameters, + ...carrierParameters, + ]), + semanticParameters: Object.freeze([...semanticParameters, ...semanticCarrierParameters]), + callParameters: Object.freeze([...contractParameters, ...semanticCarrierParameters]), result, semanticResult, }) diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index b583b9173..533e41652 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -1860,6 +1860,7 @@ export const catalog = ( } for (const child of Hir.expressionTree(expression)) { if (child._tag === 'BuiltinCall') { + if (Scalar.isCheckedOperation(child.operation)) addReferenced('bool') for (const argument of child.typeArguments) { const specialized = Type.substituteGenericArgument(argument, substitution) if (Type.isTypeArgument(specialized)) addReferenced(specialized) @@ -1948,6 +1949,7 @@ const addExpressionTypes = ( const specialized = Type.substitute(expression.type, substitution) types.set(Type.key(specialized), specialized) if (expression._tag === 'BuiltinCall') { + if (Scalar.isCheckedOperation(expression.operation)) types.set(Type.key('bool'), 'bool') for (const argument of expression.typeArguments) { const specialized = Type.substituteGenericArgument(argument, substitution) const type = Type.isTypeArgument(specialized) diff --git a/packages/compiler/src/LowerBuiltin.ts b/packages/compiler/src/LowerBuiltin.ts index 5de50c9ef..2b4079891 100644 --- a/packages/compiler/src/LowerBuiltin.ts +++ b/packages/compiler/src/LowerBuiltin.ts @@ -1,4 +1,10 @@ -import { authored, cleanupForLocal, concreteCleanup, generated } from './CleanupEmission.js' +import { + authored, + callableLocalCleanup, + cleanupForLocal, + concreteCleanup, + generated, +} from './CleanupEmission.js' import type * as DeclarationFacts from './DeclarationFacts.js' import type { LoweredExpression } from './EffectLowering.js' import type {} from './EntryAssembly.js' @@ -818,7 +824,13 @@ export const lowerBuiltinExpression = ( if (expression.operation === 'StringEqualsExact') return undefined const conversionTarget = Scalar.conversionTarget(expression.operation) if (Scalar.isCheckedOperation(expression.operation)) { - const [first] = argumentLocals + const arity = expression.operation.startsWith('CheckedConvertTo') ? 1 : 2 + const operands = Object.freeze(argumentLocals.slice(0, arity)) + const present = argumentLocals.at(arity) + const absent = argumentLocals.at(arity + 1) + const presentType = present === undefined ? undefined : fn.localTypes.at(present.ordinal) + const absentType = absent === undefined ? undefined : fn.localTypes.at(absent.ordinal) + const [first] = operands const sourceType = first === undefined ? undefined : fn.localTypes.at(first.ordinal) const semanticSource = sourceType === undefined ? undefined : Mir.semanticType(sourceType) const sourceScalar = @@ -830,32 +842,35 @@ export const lowerBuiltinExpression = ( const targetType = fn.type(expression.type) if ( first === undefined || + present === undefined || + absent === undefined || + presentType?._tag !== 'CallableValue' || + absentType?._tag !== 'CallableValue' || sourceScalar?.category !== 'Integer' || (valueScalar?.category !== 'Integer' && valueScalar?.category !== 'Character') || sourceType?._tag !== sourceScalar.spelling || - targetType?._tag !== 'Union' || - argumentLocals.some((local) => fn.localTypes.at(local.ordinal)?._tag !== sourceType._tag) - ) - return undefined - const success = Type.some(valueScalar.spelling) - const failure = Type.none - if ( - !targetType.type.members.some((member) => Type.equals(member, success)) || - !targetType.type.members.some((member) => Type.equals(member, failure)) + targetType === undefined || + operands.some((local) => fn.localTypes.at(local.ordinal)?._tag !== sourceType._tag) ) return undefined + const valid = fn.alloc(Object.freeze({ _tag: 'bool' as const })) + const value = fn.alloc(Object.freeze({ _tag: valueScalar.spelling })) const destination = fn.alloc(targetType) fn.emit( Object.freeze({ _tag: 'CheckedScalar' as const, operation: expression.operation, destination, - operands: Object.freeze(argumentLocals), + valid, + value, + operands, + present, + absent, + presentCleanup: callableLocalCleanup(fn, presentType), + absentCleanup: callableLocalCleanup(fn, absentType), sourceType, valueType: Object.freeze({ _tag: valueScalar.spelling }), type: targetType, - success, - failure, provenance: authored(expression.span), }), ) diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 8ac4a0278..4d4c62f81 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -413,7 +413,7 @@ export function lowerExpressionInner( _tag: 'MakeCallable', destination, target: expression.target, - typeArguments: Object.freeze([]), + typeArguments: expression.typeArguments, captures: Object.freeze([]), type, provenance: authored(expression.span), @@ -547,8 +547,7 @@ export function lowerExpressionInner( if (!lowered || type === undefined || callableType === undefined) return undefined if ( realizedTarget?._tag === 'BuiltinCallableTarget' && - Scalar.isCheckedOperation(realizedTarget.operation) && - type._tag === 'Union' + Scalar.isCheckedOperation(realizedTarget.operation) ) { const actorScalar = Scalar.find(realizedTarget.actor) const scalarOperation = actorScalar?.operations.find( @@ -563,39 +562,53 @@ export function lowerExpressionInner( : (Scalar.conversionTarget(realizedTarget.operation) ?? actorScalar) const realizedCaptures = definition?.captures ?? captures const ordered: Array = Array.from({ - length: scalarOperation?.arity ?? 0, + length: (scalarOperation?.arity ?? 0) + 2, }) for (const capture of realizedCaptures) ordered[capture.parameterOrdinal] = capture.source for (const argument of arguments_) { const empty = ordered.indexOf(undefined) if (empty >= 0) ordered[empty] = argument } - const operands = ordered.filter((operand): operand is Mir.LocalId => operand !== undefined) + const operands = ordered + .slice(0, scalarOperation?.arity ?? 0) + .filter((operand): operand is Mir.LocalId => operand !== undefined) + const present = ordered.at(scalarOperation?.arity ?? -1) + const absent = ordered.at((scalarOperation?.arity ?? -1) + 1) const first = operands.at(0) const sourceType = first === undefined ? undefined : fn.localTypes.at(first.ordinal) + const presentType = present === undefined ? undefined : fn.localTypes.at(present.ordinal) + const absentType = absent === undefined ? undefined : fn.localTypes.at(absent.ordinal) if ( sourceScalar?.category !== 'Integer' || (valueScalar?.category !== 'Integer' && valueScalar?.category !== 'Character') || scalarOperation === undefined || + present === undefined || + absent === undefined || + presentType?._tag !== 'CallableValue' || + absentType?._tag !== 'CallableValue' || operands.length !== scalarOperation.arity || sourceType?._tag !== sourceScalar.spelling || operands.some((operand) => fn.localTypes.at(operand.ordinal)?._tag !== sourceType._tag) ) return undefined - const success = Type.some(valueScalar.spelling) - const failure = Type.none + const valid = fn.alloc(Object.freeze({ _tag: 'bool' as const })) + const value = fn.alloc(Object.freeze({ _tag: valueScalar.spelling })) const destination = fn.alloc(type) fn.emit( Object.freeze({ _tag: 'CheckedScalar' as const, operation: scalarOperation.code, destination, + valid, + value, operands: Object.freeze(operands), + present, + absent, + presentCleanup: callableLocalCleanup(fn, presentType), + absentCleanup: callableLocalCleanup(fn, absentType), sourceType, valueType: Object.freeze({ _tag: valueScalar.spelling }), type, - success, - failure, provenance: authored(expression.span), }), ) diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index 9bad7de0a..1163bd39f 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -82,6 +82,7 @@ export type Type = readonly _tag: 'CallableValue' readonly type: SilkType.Callable readonly target: Hir.CallableTarget + readonly typeArguments?: ReadonlyArray readonly site?: Hir.CallableSiteId readonly environment?: Extract< Layout.CallableEnvironment, @@ -423,12 +424,16 @@ export type Operation = readonly _tag: 'CheckedScalar' readonly operation: Scalar.OperationCode readonly destination: LocalId + readonly valid: LocalId + readonly value: LocalId readonly operands: ReadonlyArray + readonly present: LocalId + readonly absent: LocalId + readonly presentCleanup: CleanupPlan.CleanupPlan + readonly absentCleanup: CleanupPlan.CleanupPlan readonly sourceType: ScalarType readonly valueType: ScalarType - readonly type: Extract - readonly success: SilkType.Nominal - readonly failure: SilkType.Nominal + readonly type: Type readonly provenance: Provenance } | { diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index 50f816b49..3b6ed9a80 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -36,7 +36,22 @@ export type LinearTerminator = | { readonly _tag: 'Trap'; readonly reason: string; readonly provenance: Mir.Provenance } export type LinearOperation = - | Exclude + | Exclude< + Mir.Operation, + { + readonly _tag: 'Match' | 'ShortCircuit' | 'CheckedScalar' | 'PropagateEffectFailure' + } + > + | { + readonly _tag: 'CheckedScalarOutcome' + readonly operation: Extract['operation'] + readonly valid: Mir.LocalId + readonly value: Mir.LocalId + readonly operands: ReadonlyArray + readonly sourceType: Mir.ScalarType + readonly valueType: Mir.ScalarType + readonly provenance: Mir.Provenance + } | { readonly _tag: 'BindMatch' readonly scrutinee: Mir.LocalId @@ -51,6 +66,7 @@ export const isLinearOperation = ( ): operation is LinearOperation => operation._tag !== 'Match' && operation._tag !== 'ShortCircuit' && + operation._tag !== 'CheckedScalar' && operation._tag !== 'PropagateEffectFailure' export const linearOperations = ( @@ -91,7 +107,6 @@ export const destinationOf = (operation: LinearOperation): Mir.LocalId | undefin case 'StringEqualsExact': case 'Binary': case 'ConvertInteger': - case 'CheckedScalar': case 'Move': case 'BeginLoan': case 'SliceLength': @@ -138,6 +153,8 @@ export const destinationOf = (operation: LinearOperation): Mir.LocalId | undefin case 'SlotCopy': case 'SlotDrop': return operation.destination + case 'CheckedScalarOutcome': + return operation.value case 'BindMatch': return operation.binding.destination case 'CheckPlace': @@ -209,6 +226,7 @@ export const expandMatches = ( (operation) => operation._tag === 'Match' || operation._tag === 'ShortCircuit' || + operation._tag === 'CheckedScalar' || operation._tag === 'PropagateEffectFailure', ) if (specialIndex < 0) { @@ -236,6 +254,89 @@ export const expandMatches = ( ) return } + if (special?._tag === 'CheckedScalar') { + const presentType = fn.localTypes.at(special.present.ordinal) + const absentType = fn.localTypes.at(special.absent.ordinal) + if (presentType?._tag !== 'CallableValue' || absentType?._tag !== 'CallableValue') + throw new RangeError('LLVM checked scalar expansion lost its carrier callables') + const following = reserve() + const present = reserve() + const absent = reserve() + const apply = ( + callable: Mir.LocalId, + callableType: Extract, + arguments_: ReadonlyArray, + ): Extract => + Object.freeze({ + _tag: 'ApplyCallable', + destination: special.destination, + callable, + typeArguments: + callableType.environment?.callable.typeArguments ?? + callableType.storage?.realization.targetArguments ?? + callableType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: arguments_, + callableType: callableType.type, + access: callableType.type.mode, + evaluation: 'CalleeThenArguments', + realization: 'Environment', + type: special.type, + provenance: special.provenance, + }) + const drop = ( + local: Mir.LocalId, + cleanup: Extract['cleanup'], + ): Extract => + Object.freeze({ _tag: 'Drop', local, cleanup, provenance: special.provenance }) + blocks.push( + Object.freeze({ + id, + origin, + kind, + operations: linearOperations([ + ...operations.slice(0, specialIndex), + Object.freeze({ + _tag: 'CheckedScalarOutcome' as const, + operation: special.operation, + valid: special.valid, + value: special.value, + operands: special.operands, + sourceType: special.sourceType, + valueType: special.valueType, + provenance: special.provenance, + }), + ]), + terminator: Object.freeze({ + _tag: 'Branch', + condition: special.valid, + taken: present, + otherwise: absent, + provenance: special.provenance, + }), + }), + ) + lowerSequence(following, origin, kind, operations.slice(specialIndex + 1), terminator) + lowerSequence( + present, + origin, + 'Normal', + [ + drop(special.absent, special.absentCleanup), + apply(special.present, presentType, Object.freeze([special.value])), + ], + jump(following, special.provenance), + ) + lowerSequence( + absent, + origin, + 'Normal', + [drop(special.present, special.presentCleanup), apply(special.absent, absentType, [])], + jump(following, special.provenance), + ) + return + } if (special?._tag === 'ShortCircuit') { const following = reserve() const evaluateRight = reserve() diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 3934800b6..ef6c07831 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -674,7 +674,14 @@ export const operationLocals = (operation: Operation): ReadonlyArray => case 'FloatTranscendental': return [operation.destination, operation.source] case 'CheckedScalar': - return [operation.destination, ...operation.operands] + return [ + operation.destination, + operation.valid, + operation.value, + ...operation.operands, + operation.present, + operation.absent, + ] case 'ValidateLayout': return [operation.destination, operation.bytes, operation.alignment] case 'RepeatLayout': @@ -1527,8 +1534,9 @@ const operationTypes = (operation: Operation): ReadonlyArray => { case 'FloatTranscendental': return [operation.source] case 'CheckedScalar': - return operation.operands + return [...operation.operands, operation.present, operation.absent] case 'ValidateLayout': return [operation.bytes, operation.alignment] case 'RepeatLayout': @@ -3261,6 +3269,10 @@ export const verify = (self: Module): ReadonlyArray => { } if (operation._tag === 'CheckedScalar') { const destination = fn.localTypes.at(operation.destination.ordinal) + const valid = fn.localTypes.at(operation.valid.ordinal) + const value = fn.localTypes.at(operation.value.ordinal) + const present = fn.localTypes.at(operation.present.ordinal) + const absent = fn.localTypes.at(operation.absent.ordinal) const operands = operation.operands.map((operand) => fn.localTypes.at(operand.ordinal)) const sourceScalar = Scalar.find(operation.sourceType._tag) const valueScalar = Scalar.find(operation.valueType._tag) @@ -3273,19 +3285,22 @@ export const verify = (self: Module): ReadonlyArray => { if ( (!characterConversion && !integerOperation) || destination === undefined || + valid?._tag !== 'bool' || + value?._tag !== operation.valueType._tag || + present?._tag !== 'CallableValue' || + absent?._tag !== 'CallableValue' || operands.length < 1 || operands.some( (operand) => operand === undefined || !SilkType.equals(semanticType(operand), operation.sourceType._tag), ) || - !SilkType.equals(semanticType(destination), operation.type.type) || - !operation.type.type.members.some((member) => - SilkType.equals(member, operation.success), - ) || - !operation.type.type.members.some((member) => - SilkType.equals(member, operation.failure), - ) + !SilkType.equals(semanticType(destination), semanticType(operation.type)) || + present.type.parameters.length !== 1 || + !SilkType.equals(present.type.parameters[0] ?? 'never', operation.valueType._tag) || + !SilkType.equals(present.type.result, semanticType(operation.type)) || + absent.type.parameters.length !== 0 || + !SilkType.equals(absent.type.result, semanticType(operation.type)) ) violations.push( Object.freeze({ @@ -3293,7 +3308,7 @@ export const verify = (self: Module): ReadonlyArray => { rule: 'InvalidIntegerOperation', function: fn.id, region: region.id, - detail: 'checked scalar operation has inconsistent operands or Option result', + detail: 'checked scalar operation has inconsistent operands or carrier result', }), ) } diff --git a/packages/compiler/src/NativeOperation.ts b/packages/compiler/src/NativeOperation.ts index 8d68ee215..ca739cbb2 100644 --- a/packages/compiler/src/NativeOperation.ts +++ b/packages/compiler/src/NativeOperation.ts @@ -111,7 +111,7 @@ export const emit = Effect.fnUntraced(function* ( case 'ReinterpretScalar': case 'FloatUnary': case 'FloatTranscendental': - case 'CheckedScalar': + case 'CheckedScalarOutcome': case 'Binary': return yield* NativeScalarOperation.emit(context.scalar, operation) case 'Drop': diff --git a/packages/compiler/src/NativeScalarOperation.ts b/packages/compiler/src/NativeScalarOperation.ts index c31377ff5..e2b893391 100644 --- a/packages/compiler/src/NativeScalarOperation.ts +++ b/packages/compiler/src/NativeScalarOperation.ts @@ -14,7 +14,6 @@ import * as NativeStorage from './NativeStorage.js' import * as NativeTranscendental from './NativeTranscendental.js' import * as NativeType from './NativeType.js' import * as Scalar from './Scalar.js' -import * as SilkType from './Type.js' type Operation = Extract< LinearOperation, @@ -25,7 +24,7 @@ type Operation = Extract< | 'ReinterpretScalar' | 'FloatUnary' | 'FloatTranscendental' - | 'CheckedScalar' + | 'CheckedScalarOutcome' | 'Binary' } > @@ -388,7 +387,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op nativeStorage.locals.set(operation.destination.ordinal, Object.freeze([result])) break } - case 'CheckedScalar': { + case 'CheckedScalarOutcome': { const leftLocal = operation.operands.at(0) const rightLocal = operation.operands.at(1) const source = Scalar.find(operation.sourceType._tag) @@ -411,7 +410,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op const targetBits = Scalar.bits(target, pointerBits) const sourcePhysical = integerTypes.get(sourceBits) ?? i32 const targetPhysical = integerTypes.get(targetBits) ?? i32 - const name = `checked${operation.destination.ordinal}` + const name = `checked${operation.value.ordinal}` let result: Value.Input let invalid: Value.Input if (characterConversion) { @@ -589,29 +588,11 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op } else { throw new RangeError('LLVM checked division lost its integer target') } - const successOrdinal = operation.type.type.members.findIndex((member) => - SilkType.equals(member, operation.success), - ) - const failureOrdinal = operation.type.type.members.findIndex((member) => - SilkType.equals(member, operation.failure), - ) - if (successOrdinal < 0 || failureOrdinal < 0) - throw new RangeError('LLVM checked scalar operation lost its Option members') - const successTag = yield* Constant.integerSigned(builder, i32, BigInt(successOrdinal)) - const failureTag = yield* Constant.integerSigned(builder, i32, BigInt(failureOrdinal)) - const tag = yield* FunctionBody.select(body, invalid, failureTag, successTag, `${name}_tag`) - const valueLane = NativeType.lanesFor(types, operation.valueType).at(0) - const payloadLane = NativeType.lanesFor(types, operation.type).at(1) - if (valueLane === undefined || payloadLane === undefined) - throw new RangeError('LLVM checked scalar operation lost its payload lane') - const payload = yield* NativeArith.coerceLane( - arith.lane, - result, - valueLane, - payloadLane, - `${name}_payload`, - ) - nativeStorage.locals.set(operation.destination.ordinal, Object.freeze([tag, payload])) + const zero = yield* Constant.integerUnsigned(builder, i32, 0n) + const one = yield* Constant.integerUnsigned(builder, i32, 1n) + const valid = yield* FunctionBody.select(body, invalid, zero, one, `${name}_valid`) + nativeStorage.locals.set(operation.valid.ordinal, Object.freeze([valid])) + nativeStorage.locals.set(operation.value.ordinal, Object.freeze([result])) break } case 'Binary': { diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index fea6a3d73..43b0fb528 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -54,7 +54,7 @@ export const modules = [ module: 'silk/char', path: 'silk/char.silk', sourceIdentity: 'silk/char', - digest: '7e86e1e9c31b4b38d6a4e637693e8e543a0df7123485677011007a51f5ad9eee', + digest: 'ee96c7678f378ed6226fa4502c80042745795af36e7ad5897728934110f617eb', documentation: 'silk/char.silk', layer: 'portable', runtimeInventory: [ @@ -69,13 +69,13 @@ export const modules = [ ], namespace: 'char', source: - "//! Checked construction, integer inspection, equality, and ordering for Unicode scalar `char`\n//! values.\n//!\n//! # When to use\n//! Prefer comparison operators in direct expressions. Import these functions when comparison must\n//! be passed by name or when the Unicode scalar ordering should be explicit at a call site.\n//!\n//! # Details\n//! Ordering compares scalar values, not locale collation, normalization, grapheme clusters, or UTF-8\n//! byte sequences. These functions allocate no storage and add no Effect channels.\n//!\n//! # Gotchas\n//! Integer construction rejects `0xD800` through `0xDFFF` and values above `0x10FFFF`. It returns\n//! `None`; it does not truncate or trap.\n//!\n//! # Examples\n//! ## Validate a Unicode scalar integer\n//! ```silk\n//! import silk.char as Char\n//!\n//! import silk.option as Option\n//!\n//! pub fn main() -> i32 {\n//! let scalar = Char.fromU32(0x2603)\n//! let value = move scalar\n//! |> Option.unwrapOr('?')\n//! if Char.toU32(value) != 0x2603 {\n//! return 1\n//! }\n//! let surrogate = Char.fromU32(0xD800)\n//! let replacement = move surrogate\n//! |> Option.unwrapOr('?')\n//! if replacement != '?' {\n//! return 2\n//! }\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.option { Option }\nimport silk.u32 as u32\n\n/// Converts an integer to a Unicode scalar. Returns `None` for `0xD800` through `0xDFFF` and values\n/// above `0x10FFFF`.\npub fn fromU32(value: u32) -> Option {\n return Intrinsic.charFromU32(value)\n}\n\n/// Returns the exact integer value of an already valid Unicode scalar.\npub fn toU32(value: char) -> u32 {\n return Intrinsic.charToU32(value)\n}\n\n/// Returns `true` when `left` and `right` are the same Unicode scalar value.\npub fn equals(left: char, right: char) -> bool {\n return Intrinsic.charEquals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are different Unicode scalar values.\npub fn notEquals(left: char, right: char) -> bool {\n return Intrinsic.charNotEquals(left, right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is less than `right`.\npub fn lessThan(left: char, right: char) -> bool {\n return Intrinsic.charLessThan(&left, &right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is less than or equal to `right`.\npub fn lessOrEqual(left: char, right: char) -> bool {\n return Intrinsic.charLessOrEqual(left, right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is greater than `right`.\npub fn greaterThan(left: char, right: char) -> bool {\n return Intrinsic.charGreaterThan(left, right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: char, right: char) -> bool {\n return Intrinsic.charGreaterOrEqual(left, right)\n}\n", + "//! Checked construction, integer inspection, equality, and ordering for Unicode scalar `char`\n//! values.\n//!\n//! # When to use\n//! Prefer comparison operators in direct expressions. Import these functions when comparison must\n//! be passed by name or when the Unicode scalar ordering should be explicit at a call site.\n//!\n//! # Details\n//! Ordering compares scalar values, not locale collation, normalization, grapheme clusters, or UTF-8\n//! byte sequences. These functions allocate no storage and add no Effect channels.\n//!\n//! # Gotchas\n//! Integer construction rejects `0xD800` through `0xDFFF` and values above `0x10FFFF`. It returns\n//! `None`; it does not truncate or trap.\n//!\n//! # Examples\n//! ## Validate a Unicode scalar integer\n//! ```silk\n//! import silk.char as Char\n//!\n//! import silk.option as Option\n//!\n//! pub fn main() -> i32 {\n//! let scalar = Char.fromU32(0x2603)\n//! let value = move scalar\n//! |> Option.unwrapOr('?')\n//! if Char.toU32(value) != 0x2603 {\n//! return 1\n//! }\n//! let surrogate = Char.fromU32(0xD800)\n//! let replacement = move surrogate\n//! |> Option.unwrapOr('?')\n//! if replacement != '?' {\n//! return 2\n//! }\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.option { Option, none, some }\nimport silk.u32 as u32\n\n/// Converts an integer to a Unicode scalar. Returns `None` for `0xD800` through `0xDFFF` and values\n/// above `0x10FFFF`.\npub fn fromU32(value: u32) -> Option {\n return Intrinsic.charFromU32>(value, some, none)\n}\n\n/// Returns the exact integer value of an already valid Unicode scalar.\npub fn toU32(value: char) -> u32 {\n return Intrinsic.charToU32(value)\n}\n\n/// Returns `true` when `left` and `right` are the same Unicode scalar value.\npub fn equals(left: char, right: char) -> bool {\n return Intrinsic.charEquals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are different Unicode scalar values.\npub fn notEquals(left: char, right: char) -> bool {\n return Intrinsic.charNotEquals(left, right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is less than `right`.\npub fn lessThan(left: char, right: char) -> bool {\n return Intrinsic.charLessThan(&left, &right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is less than or equal to `right`.\npub fn lessOrEqual(left: char, right: char) -> bool {\n return Intrinsic.charLessOrEqual(left, right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is greater than `right`.\npub fn greaterThan(left: char, right: char) -> bool {\n return Intrinsic.charGreaterThan(left, right)\n}\n\n/// Returns `true` when the Unicode scalar value of `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: char, right: char) -> bool {\n return Intrinsic.charGreaterOrEqual(left, right)\n}\n", }, { module: 'silk/child_process', path: 'silk/child_process.silk', sourceIdentity: 'silk/child_process', - digest: '24e1b57fb3c1ad26d6dba3d3bc60d12a93fe40e0dcbf97f52c8736578805c67e', + digest: 'dbbb0d100aed8b89eb44d60ddd038eb684d913336245b22dd3c27cfb3c8b6bfa', documentation: 'silk/child_process.silk', layer: 'portable', runtimeInventory: [], @@ -90,13 +90,13 @@ export const modules = [ 'Signaled', ], source: - "//! Portable blocking child execution from structured byte arguments to fully captured output.\n//!\n//! # When to use\n//! Build a [`ProcessRequest`] when one executable should run directly. This API is not a shell:\n//! spaces, quotes, and metacharacters in an argument remain data and are never parsed as command\n//! syntax.\n//!\n//! # Details\n//! Requests preserve NUL-free argument and environment-entry bytes in insertion order. The\n//! environment begins empty, and the working directory is inherited unless [`requestWithin`]\n//! selects one. Child standard input is closed. `ChildProcess.execute` blocks until termination and\n//! owns complete stdout and stderr captures in [`ProcessOutcome`]. A nonzero exit is outcome data.\n//!\n//! [`ProcessError`] is reserved for failing to spawn, wait, or capture; it carries a stable\n//! portable reason and may retain a provider code. Execution also reports [`OutOfMemoryError`] when\n//! captured output cannot be owned.\n//!\n//! # Gotchas\n//! An argument, environment name, or environment value must not contain NUL. The request uses NUL\n//! as its entry separator, and a native provider cannot preserve an embedded NUL as data.\n//!\n//! # Examples\n//! ## Handle a nonzero child exit as outcome data\n//!\n//! ```silk\n//! import silk.bytes as Bytes\n//!\n//! import silk.child_process as Process\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as Path\n//!\n//! import silk.option as Option\n//!\n//! struct Completed {}\n//!\n//! effect fn execute(self: &mut Completed, request: &Process.ProcessRequest) -> Process.ProcessOutcome\n//! ! Process.ProcessError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! return Process.exited(7, Bytes.make(), Bytes.make())\n//! }\n//!\n//! impl Process.ChildProcess for Completed {\n//! execute: Completed.execute\n//! }\n//!\n//! effect fn program() -> i32\n//! ! Path.FileError | Allocator.OutOfMemoryError | Process.ProcessError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut provider = Completed {}\n//! let path = run Path.make(\"/tool\")\n//! |> Effect.provideMut(&mut allocator)\n//! let request = run Process.request(&path)\n//! |> Effect.provideMut(&mut allocator)\n//! let outcome = run Process.submit(&request)\n//! |> Effect.provideMut(&mut provider)\n//! |> Effect.provideMut(&mut allocator)\n//! return match move Process.exitCode(&outcome) {\n//! Option.Some {value} => 35 + value\n//! Option.None {} => 1\n//! }\n//! }\n//!\n//! effect fn recover(error: Path.FileError | Allocator.OutOfMemoryError | Process.ProcessError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\n// The portable child-process boundary: one blocking execution from structured input to structured\n// output. It is not a shell. The request carries an executable path and ordered argument bytes, and\n// no part of it is ever parsed as a command line, so a space, a quote, or a metacharacter inside an\n// argument reaches the child as data.\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asSlice as bytesSlice,\n copy as bytesCopy,\n length as bytesLength,\n make as bytesMake\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem { Path, rawBytes as pathRawBytes }\nimport silk.i32 as i32\nimport silk.option { None, Option, Some, none, some }\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A provider-reported execution stage for one [`ProcessError`].\npub struct ProcessOperation {\n /// The stable numeric code for the provider-reported execution stage.\n pub code: i32\n}\n\n/// A portable recovery category for one [`ProcessError`].\npub struct ProcessReason {\n /// The stable numeric code for the portable recovery category.\n pub code: i32\n}\n\n/// A typed failure to start, wait for, or capture one child process.\n///\n/// # Details\n///\n/// `operation` identifies the provider-reported stage. `reason` gives a portable recovery category.\n/// Use [`providerCode`] when diagnostics also need a provider-defined numeric code.\n///\n/// A child that exits with a nonzero code produces [`ProcessOutcome`] data instead of this error.\npub struct ProcessError {\n /// The execution stage reported by the provider.\n pub operation: ProcessOperation\n /// The portable category that callers can use for recovery.\n pub reason: ProcessReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Returns the stage for preparing and starting the child process.\npub fn spawnOperation() -> ProcessOperation { return ProcessOperation { code: 0 } }\n\n/// Returns the stage for waiting until the child process terminates.\npub fn waitOperation() -> ProcessOperation { return ProcessOperation { code: 1 } }\n\n/// Returns the stage for copying the completed output and error streams.\npub fn captureOperation() -> ProcessOperation { return ProcessOperation { code: 2 } }\n\n/// Returns the stable numeric code for an execution stage.\npub fn operationCode(operation: ProcessOperation) -> i32 { return operation.code }\n\n/// Returns the reason used when no executable exists at the requested path.\npub fn notFound() -> ProcessReason { return ProcessReason { code: 0 } }\n\n/// Returns the reason used when the provider denies access to the requested executable.\npub fn permissionDenied() -> ProcessReason { return ProcessReason { code: 1 } }\n\n/// Returns the reason used when the provider cannot present the request to its process boundary.\npub fn invalidRequest() -> ProcessReason { return ProcessReason { code: 2 } }\n\n/// Returns the reason used when process setup or captured output exhausts provider storage.\npub fn noSpace() -> ProcessReason { return ProcessReason { code: 3 } }\n\n/// Returns the reason used when the provider does not support the requested process operation.\npub fn unsupported() -> ProcessReason { return ProcessReason { code: 4 } }\n\n/// Returns the reason used when no other portable recovery category applies.\npub fn other() -> ProcessReason { return ProcessReason { code: 5 } }\n\n/// Returns the stable numeric code for a portable recovery category.\npub fn reasonCode(reason: ProcessReason) -> i32 { return reason.code }\n\n/// Creates a process failure without a provider-defined numeric code.\npub fn failure(operation: ProcessOperation, reason: ProcessReason) -> ProcessError {\n return ProcessError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false\n }\n}\n\n/// Creates a process failure with a provider-defined numeric code for diagnostics.\npub fn failureWithCode(\n operation: ProcessOperation,\n reason: ProcessReason,\n code: i32\n) -> ProcessError {\n return ProcessError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true\n }\n}\n\n/// Returns the provider-defined numeric code, or `None` when the failure has no such code.\npub fn providerCode(error: &ProcessError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\n/// One owned child-process request with ordered arguments and an explicit environment.\n///\n/// # Details\n///\n/// Arguments and environment entries are exact platform bytes rather than checked text, so a value\n/// received from the platform can be handed to a child unchanged. Entries are retained in the order\n/// they were added, and the environment starts empty: a child sees no variable that this request\n/// did not name.\n///\n/// # Gotchas\n///\n/// Argument, environment-name, and environment-value bytes must not contain NUL. NUL separates\n/// entries in the provider request format.\npub struct ProcessRequest {\n programValue: Bytes\n argumentValues: Bytes\n argumentTotal: usize\n environmentValues: Bytes\n environmentTotal: usize\n directoryValue: Bytes\n}\n\neffect fn terminate(target: &mut Bytes) -> () ! OutOfMemoryError ? &mut Allocator {\n let terminator = [u8.toU8(0)]\n let appended = run bytesAppend(move target, &terminator)\n return ()\n}\n\n/// Creates a request for `program` with no arguments, an empty environment, and the caller's\n/// own working directory.\npub effect fn request(program: &Path) -> ProcessRequest ! OutOfMemoryError ? &mut Allocator {\n let owned = run bytesCopy(pathRawBytes(program))\n return ProcessRequest {\n programValue: move owned,\n argumentValues: bytesMake(),\n argumentTotal: usize.ZERO,\n environmentValues: bytesMake(),\n environmentTotal: usize.ZERO,\n directoryValue: bytesMake()\n }\n}\n\n/// Creates a request that runs `program` in `directory` instead of the caller's\n/// working directory.\npub effect fn requestWithin(\n program: &Path,\n directory: &Path\n) -> ProcessRequest ! OutOfMemoryError ? &mut Allocator {\n let mut built = run request(program)\n let appended = run bytesAppend(&mut built.directoryValue, pathRawBytes(directory))\n return move built\n}\n\n/// Appends one NUL-free byte argument after all arguments already in the request.\n///\n/// # Details\n///\n/// The request copies `value` and preserves argument order.\n///\n/// # Gotchas\n///\n/// `value` must not contain NUL. NUL is the entry separator used by process providers.\n/// If allocation fails, do not reuse `self`; it can contain an incomplete argument entry.\npub effect fn addArgument(\n self: &mut ProcessRequest,\n value: &[u8]\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let appended = run bytesAppend(&mut self.argumentValues, value)\n let terminated = run terminate(&mut self.argumentValues)\n self.argumentTotal = self.argumentTotal + usize.ONE\n return ()\n}\n\n/// Appends one NUL-free environment entry as `name`, `=`, and `value` bytes.\n///\n/// # Details\n///\n/// The request starts with an empty environment and preserves insertion order. This function does\n/// not read or merge the caller's environment.\n///\n/// # Gotchas\n///\n/// `name` and `value` must not contain NUL. The request builder does not validate environment-name\n/// grammar beyond this provider-format requirement.\n/// If allocation fails, do not reuse `self`; it can contain an incomplete environment entry.\npub effect fn setVariable(\n self: &mut ProcessRequest,\n name: &[u8],\n value: &[u8]\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let appendedName = run bytesAppend(&mut self.environmentValues, name)\n let separator = [u8.toU8(61)]\n let appendedSeparator = run bytesAppend(&mut self.environmentValues, &separator)\n let appendedValue = run bytesAppend(&mut self.environmentValues, value)\n let terminated = run terminate(&mut self.environmentValues)\n self.environmentTotal = self.environmentTotal + usize.ONE\n return ()\n}\n\n/// Borrows the executable path bytes for the lifetime of the request borrow.\npub fn program(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.programValue)\n}\n\n/// Borrows all ordered arguments as one block of NUL-terminated entries.\npub fn arguments(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.argumentValues)\n}\n\n/// Returns the number of calls to [`addArgument`] that completed successfully.\npub fn argumentCount(self: &ProcessRequest) -> usize {\n return self.argumentTotal\n}\n\n/// Borrows the explicit environment as one block of NUL-terminated `name=value` entries.\npub fn environment(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.environmentValues)\n}\n\n/// Returns the number of calls to [`setVariable`] that completed successfully.\npub fn environmentCount(self: &ProcessRequest) -> usize {\n return self.environmentTotal\n}\n\n/// Borrows the selected working-directory bytes, or an empty view when the child inherits one.\npub fn workingDirectory(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.directoryValue)\n}\n\n/// Reports whether the request selects a working directory instead of inheriting one.\npub fn hasWorkingDirectory(self: &ProcessRequest) -> bool {\n return bytesLength(&self.directoryValue) != usize.ZERO\n}\n\n/// A completed child process that returned an exit code and two owned captures.\npub struct Exited {\n /// The code the child returned. Any value, including a nonzero one, is ordinary data.\n pub code: i32\n /// The complete captured standard output, owned by this outcome.\n pub output: Bytes\n /// The complete captured standard error, owned by this outcome.\n pub errors: Bytes\n}\n\n/// A completed child process that a signal terminated, with two owned captures.\npub struct Signaled {\n /// The platform signal number that terminated the child.\n pub signal: i32\n /// The complete captured standard output, owned by this outcome.\n pub output: Bytes\n /// The complete captured standard error, owned by this outcome.\n pub errors: Bytes\n}\n\n/// One completed child execution, represented as an exit or signal termination.\n///\n/// # Details\n///\n/// An exit code and a terminating signal are distinct members rather than one integer, so a caller\n/// can never read a signal number as though it were an exit code.\npub struct ProcessOutcome {\n /// The completed outcome.\n pub value: Exited | Signaled\n}\n\n/// A portable blocking child-process service with complete output capture.\n///\n/// # Details\n///\n/// `execute` closes child standard input and blocks until termination. The returned outcome owns\n/// complete standard-output and standard-error captures. A nonzero exit code is outcome data.\n///\n/// A start, wait, or capture failure produces [`ProcessError`]. Owning either capture can also\n/// produce [`OutOfMemoryError`]. The operation needs exclusive provider and allocator requirements.\npub service ChildProcess {\n /// Runs one request to termination and returns owned captures of both output streams.\n ///\n /// # Details\n ///\n /// This operation blocks and closes the child's standard input. A nonzero exit code returns on\n /// the success channel. Start, wait, and capture failures produce `ProcessError`.\n effect fn execute(\n request: &ProcessRequest\n ) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut ChildProcess | &mut Allocator\n}\n\n/// Creates an exited outcome that owns `output` and `errors`.\npub fn exited(code: i32, output: Bytes, errors: Bytes) -> ProcessOutcome {\n return ProcessOutcome {\n value: Exited { code: code, output: move output, errors: move errors }\n }\n}\n\n/// Creates a signaled outcome that owns `output` and `errors`.\npub fn signaled(signal: i32, output: Bytes, errors: Bytes) -> ProcessOutcome {\n return ProcessOutcome {\n value: Signaled { signal: signal, output: move output, errors: move errors }\n }\n}\n\n/// Reports whether a signal terminated the child instead of an exit code.\npub fn isSignaled(outcome: &ProcessOutcome) -> bool {\n return match &outcome.value {\n Exited { code, output, errors } => false\n Signaled { signal, output, errors } => true\n }\n}\n\n/// Returns the exit code, or `None` when a signal terminated the child.\npub fn exitCode(outcome: &ProcessOutcome) -> Option {\n return match &outcome.value {\n Exited { code, output, errors } => some(code)\n Signaled { signal, output, errors } => none()\n }\n}\n\n/// Returns the terminating signal number, or `None` when the child returned an exit code.\npub fn terminatingSignal(outcome: &ProcessOutcome) -> Option {\n return match &outcome.value {\n Exited { code, output, errors } => none()\n Signaled { signal, output, errors } => some(signal)\n }\n}\n\n/// Borrows the complete captured standard output without consuming the outcome.\npub fn outputBytes(outcome: &ProcessOutcome) -> &[u8] {\n return match &outcome.value {\n Exited { code, output, errors } => bytesSlice(&output)\n Signaled { signal, output, errors } => bytesSlice(&output)\n }\n}\n\n/// Borrows the complete captured standard error without consuming the outcome.\npub fn errorBytes(outcome: &ProcessOutcome) -> &[u8] {\n return match &outcome.value {\n Exited { code, output, errors } => bytesSlice(&errors)\n Signaled { signal, output, errors } => bytesSlice(&errors)\n }\n}\n\n/// Runs the active [`ChildProcess`] provider for one request.\n///\n/// # Details\n///\n/// This wrapper preserves the service contract: it blocks, closes child input, and returns complete\n/// owned captures. It requires exclusive child-process and allocator providers.\npub effect fn submit(\n request: &ProcessRequest\n) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut ChildProcess | &mut Allocator {\n return run ChildProcess.execute(request)\n}\n", + "//! Portable blocking child execution from structured byte arguments to fully captured output.\n//!\n//! # When to use\n//! Build a [`ProcessRequest`] when one executable should run directly. This API is not a shell:\n//! spaces, quotes, and metacharacters in an argument remain data and are never parsed as command\n//! syntax.\n//!\n//! # Details\n//! Requests preserve NUL-free argument and environment-entry bytes in insertion order. The\n//! environment begins empty, and the working directory is inherited unless [`requestWithin`]\n//! selects one. Child standard input is closed. `ChildProcess.execute` blocks until termination and\n//! owns complete stdout and stderr captures in [`ProcessOutcome`]. A nonzero exit is outcome data.\n//!\n//! [`ProcessError`] is reserved for failing to spawn, wait, or capture; it carries a stable\n//! portable reason and may retain a provider code. Execution also reports [`OutOfMemoryError`] when\n//! captured output cannot be owned.\n//!\n//! # Gotchas\n//! An argument, environment name, or environment value must not contain NUL. The request uses NUL\n//! as its entry separator, and a native provider cannot preserve an embedded NUL as data.\n//!\n//! # Examples\n//! ## Handle a nonzero child exit as outcome data\n//!\n//! ```silk\n//! import silk.bytes as Bytes\n//!\n//! import silk.child_process as Process\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as Path\n//!\n//! import silk.option as Option\n//!\n//! struct Completed {}\n//!\n//! effect fn execute(self: &mut Completed, request: &Process.ProcessRequest) -> Process.ProcessOutcome\n//! ! Process.ProcessError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! return Process.exited(7, Bytes.make(), Bytes.make())\n//! }\n//!\n//! impl Process.ChildProcess for Completed {\n//! execute: Completed.execute\n//! }\n//!\n//! effect fn program() -> i32\n//! ! Path.FileError | Allocator.OutOfMemoryError | Process.ProcessError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut provider = Completed {}\n//! let path = run Path.make(\"/tool\")\n//! |> Effect.provideMut(&mut allocator)\n//! let request = run Process.request(&path)\n//! |> Effect.provideMut(&mut allocator)\n//! let outcome = run Process.submit(&request)\n//! |> Effect.provideMut(&mut provider)\n//! |> Effect.provideMut(&mut allocator)\n//! return match move Process.exitCode(&outcome) {\n//! Option.Option.Some {value} => 35 + value\n//! Option.Option.None => 1\n//! }\n//! }\n//!\n//! effect fn recover(error: Path.FileError | Allocator.OutOfMemoryError | Process.ProcessError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\n// The portable child-process boundary: one blocking execution from structured input to structured\n// output. It is not a shell. The request carries an executable path and ordered argument bytes, and\n// no part of it is ever parsed as a command line, so a space, a quote, or a metacharacter inside an\n// argument reaches the child as data.\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asSlice as bytesSlice,\n copy as bytesCopy,\n length as bytesLength,\n make as bytesMake\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem { Path, rawBytes as pathRawBytes }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A provider-reported execution stage for one [`ProcessError`].\npub struct ProcessOperation {\n /// The stable numeric code for the provider-reported execution stage.\n pub code: i32\n}\n\n/// A portable recovery category for one [`ProcessError`].\npub struct ProcessReason {\n /// The stable numeric code for the portable recovery category.\n pub code: i32\n}\n\n/// A typed failure to start, wait for, or capture one child process.\n///\n/// # Details\n///\n/// `operation` identifies the provider-reported stage. `reason` gives a portable recovery category.\n/// Use [`providerCode`] when diagnostics also need a provider-defined numeric code.\n///\n/// A child that exits with a nonzero code produces [`ProcessOutcome`] data instead of this error.\npub struct ProcessError {\n /// The execution stage reported by the provider.\n pub operation: ProcessOperation\n /// The portable category that callers can use for recovery.\n pub reason: ProcessReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Returns the stage for preparing and starting the child process.\npub fn spawnOperation() -> ProcessOperation { return ProcessOperation { code: 0 } }\n\n/// Returns the stage for waiting until the child process terminates.\npub fn waitOperation() -> ProcessOperation { return ProcessOperation { code: 1 } }\n\n/// Returns the stage for copying the completed output and error streams.\npub fn captureOperation() -> ProcessOperation { return ProcessOperation { code: 2 } }\n\n/// Returns the stable numeric code for an execution stage.\npub fn operationCode(operation: ProcessOperation) -> i32 { return operation.code }\n\n/// Returns the reason used when no executable exists at the requested path.\npub fn notFound() -> ProcessReason { return ProcessReason { code: 0 } }\n\n/// Returns the reason used when the provider denies access to the requested executable.\npub fn permissionDenied() -> ProcessReason { return ProcessReason { code: 1 } }\n\n/// Returns the reason used when the provider cannot present the request to its process boundary.\npub fn invalidRequest() -> ProcessReason { return ProcessReason { code: 2 } }\n\n/// Returns the reason used when process setup or captured output exhausts provider storage.\npub fn noSpace() -> ProcessReason { return ProcessReason { code: 3 } }\n\n/// Returns the reason used when the provider does not support the requested process operation.\npub fn unsupported() -> ProcessReason { return ProcessReason { code: 4 } }\n\n/// Returns the reason used when no other portable recovery category applies.\npub fn other() -> ProcessReason { return ProcessReason { code: 5 } }\n\n/// Returns the stable numeric code for a portable recovery category.\npub fn reasonCode(reason: ProcessReason) -> i32 { return reason.code }\n\n/// Creates a process failure without a provider-defined numeric code.\npub fn failure(operation: ProcessOperation, reason: ProcessReason) -> ProcessError {\n return ProcessError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false\n }\n}\n\n/// Creates a process failure with a provider-defined numeric code for diagnostics.\npub fn failureWithCode(\n operation: ProcessOperation,\n reason: ProcessReason,\n code: i32\n) -> ProcessError {\n return ProcessError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true\n }\n}\n\n/// Returns the provider-defined numeric code, or `None` when the failure has no such code.\npub fn providerCode(error: &ProcessError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\n/// One owned child-process request with ordered arguments and an explicit environment.\n///\n/// # Details\n///\n/// Arguments and environment entries are exact platform bytes rather than checked text, so a value\n/// received from the platform can be handed to a child unchanged. Entries are retained in the order\n/// they were added, and the environment starts empty: a child sees no variable that this request\n/// did not name.\n///\n/// # Gotchas\n///\n/// Argument, environment-name, and environment-value bytes must not contain NUL. NUL separates\n/// entries in the provider request format.\npub struct ProcessRequest {\n programValue: Bytes\n argumentValues: Bytes\n argumentTotal: usize\n environmentValues: Bytes\n environmentTotal: usize\n directoryValue: Bytes\n}\n\neffect fn terminate(target: &mut Bytes) -> () ! OutOfMemoryError ? &mut Allocator {\n let terminator = [u8.toU8(0)]\n let appended = run bytesAppend(move target, &terminator)\n return ()\n}\n\n/// Creates a request for `program` with no arguments, an empty environment, and the caller's\n/// own working directory.\npub effect fn request(program: &Path) -> ProcessRequest ! OutOfMemoryError ? &mut Allocator {\n let owned = run bytesCopy(pathRawBytes(program))\n return ProcessRequest {\n programValue: move owned,\n argumentValues: bytesMake(),\n argumentTotal: usize.ZERO,\n environmentValues: bytesMake(),\n environmentTotal: usize.ZERO,\n directoryValue: bytesMake()\n }\n}\n\n/// Creates a request that runs `program` in `directory` instead of the caller's\n/// working directory.\npub effect fn requestWithin(\n program: &Path,\n directory: &Path\n) -> ProcessRequest ! OutOfMemoryError ? &mut Allocator {\n let mut built = run request(program)\n let appended = run bytesAppend(&mut built.directoryValue, pathRawBytes(directory))\n return move built\n}\n\n/// Appends one NUL-free byte argument after all arguments already in the request.\n///\n/// # Details\n///\n/// The request copies `value` and preserves argument order.\n///\n/// # Gotchas\n///\n/// `value` must not contain NUL. NUL is the entry separator used by process providers.\n/// If allocation fails, do not reuse `self`; it can contain an incomplete argument entry.\npub effect fn addArgument(\n self: &mut ProcessRequest,\n value: &[u8]\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let appended = run bytesAppend(&mut self.argumentValues, value)\n let terminated = run terminate(&mut self.argumentValues)\n self.argumentTotal = self.argumentTotal + usize.ONE\n return ()\n}\n\n/// Appends one NUL-free environment entry as `name`, `=`, and `value` bytes.\n///\n/// # Details\n///\n/// The request starts with an empty environment and preserves insertion order. This function does\n/// not read or merge the caller's environment.\n///\n/// # Gotchas\n///\n/// `name` and `value` must not contain NUL. The request builder does not validate environment-name\n/// grammar beyond this provider-format requirement.\n/// If allocation fails, do not reuse `self`; it can contain an incomplete environment entry.\npub effect fn setVariable(\n self: &mut ProcessRequest,\n name: &[u8],\n value: &[u8]\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let appendedName = run bytesAppend(&mut self.environmentValues, name)\n let separator = [u8.toU8(61)]\n let appendedSeparator = run bytesAppend(&mut self.environmentValues, &separator)\n let appendedValue = run bytesAppend(&mut self.environmentValues, value)\n let terminated = run terminate(&mut self.environmentValues)\n self.environmentTotal = self.environmentTotal + usize.ONE\n return ()\n}\n\n/// Borrows the executable path bytes for the lifetime of the request borrow.\npub fn program(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.programValue)\n}\n\n/// Borrows all ordered arguments as one block of NUL-terminated entries.\npub fn arguments(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.argumentValues)\n}\n\n/// Returns the number of calls to [`addArgument`] that completed successfully.\npub fn argumentCount(self: &ProcessRequest) -> usize {\n return self.argumentTotal\n}\n\n/// Borrows the explicit environment as one block of NUL-terminated `name=value` entries.\npub fn environment(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.environmentValues)\n}\n\n/// Returns the number of calls to [`setVariable`] that completed successfully.\npub fn environmentCount(self: &ProcessRequest) -> usize {\n return self.environmentTotal\n}\n\n/// Borrows the selected working-directory bytes, or an empty view when the child inherits one.\npub fn workingDirectory(self: &ProcessRequest) -> &[u8] {\n return bytesSlice(&self.directoryValue)\n}\n\n/// Reports whether the request selects a working directory instead of inheriting one.\npub fn hasWorkingDirectory(self: &ProcessRequest) -> bool {\n return bytesLength(&self.directoryValue) != usize.ZERO\n}\n\n/// A completed child process that returned an exit code and two owned captures.\npub struct Exited {\n /// The code the child returned. Any value, including a nonzero one, is ordinary data.\n pub code: i32\n /// The complete captured standard output, owned by this outcome.\n pub output: Bytes\n /// The complete captured standard error, owned by this outcome.\n pub errors: Bytes\n}\n\n/// A completed child process that a signal terminated, with two owned captures.\npub struct Signaled {\n /// The platform signal number that terminated the child.\n pub signal: i32\n /// The complete captured standard output, owned by this outcome.\n pub output: Bytes\n /// The complete captured standard error, owned by this outcome.\n pub errors: Bytes\n}\n\n/// One completed child execution, represented as an exit or signal termination.\n///\n/// # Details\n///\n/// An exit code and a terminating signal are distinct members rather than one integer, so a caller\n/// can never read a signal number as though it were an exit code.\npub struct ProcessOutcome {\n /// The completed outcome.\n pub value: Exited | Signaled\n}\n\n/// A portable blocking child-process service with complete output capture.\n///\n/// # Details\n///\n/// `execute` closes child standard input and blocks until termination. The returned outcome owns\n/// complete standard-output and standard-error captures. A nonzero exit code is outcome data.\n///\n/// A start, wait, or capture failure produces [`ProcessError`]. Owning either capture can also\n/// produce [`OutOfMemoryError`]. The operation needs exclusive provider and allocator requirements.\npub service ChildProcess {\n /// Runs one request to termination and returns owned captures of both output streams.\n ///\n /// # Details\n ///\n /// This operation blocks and closes the child's standard input. A nonzero exit code returns on\n /// the success channel. Start, wait, and capture failures produce `ProcessError`.\n effect fn execute(\n request: &ProcessRequest\n ) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut ChildProcess | &mut Allocator\n}\n\n/// Creates an exited outcome that owns `output` and `errors`.\npub fn exited(code: i32, output: Bytes, errors: Bytes) -> ProcessOutcome {\n return ProcessOutcome {\n value: Exited { code: code, output: move output, errors: move errors }\n }\n}\n\n/// Creates a signaled outcome that owns `output` and `errors`.\npub fn signaled(signal: i32, output: Bytes, errors: Bytes) -> ProcessOutcome {\n return ProcessOutcome {\n value: Signaled { signal: signal, output: move output, errors: move errors }\n }\n}\n\n/// Reports whether a signal terminated the child instead of an exit code.\npub fn isSignaled(outcome: &ProcessOutcome) -> bool {\n return match &outcome.value {\n Exited { code, output, errors } => false\n Signaled { signal, output, errors } => true\n }\n}\n\n/// Returns the exit code, or `None` when a signal terminated the child.\npub fn exitCode(outcome: &ProcessOutcome) -> Option {\n return match &outcome.value {\n Exited { code, output, errors } => some(code)\n Signaled { signal, output, errors } => none()\n }\n}\n\n/// Returns the terminating signal number, or `None` when the child returned an exit code.\npub fn terminatingSignal(outcome: &ProcessOutcome) -> Option {\n return match &outcome.value {\n Exited { code, output, errors } => none()\n Signaled { signal, output, errors } => some(signal)\n }\n}\n\n/// Borrows the complete captured standard output without consuming the outcome.\npub fn outputBytes(outcome: &ProcessOutcome) -> &[u8] {\n return match &outcome.value {\n Exited { code, output, errors } => bytesSlice(&output)\n Signaled { signal, output, errors } => bytesSlice(&output)\n }\n}\n\n/// Borrows the complete captured standard error without consuming the outcome.\npub fn errorBytes(outcome: &ProcessOutcome) -> &[u8] {\n return match &outcome.value {\n Exited { code, output, errors } => bytesSlice(&errors)\n Signaled { signal, output, errors } => bytesSlice(&errors)\n }\n}\n\n/// Runs the active [`ChildProcess`] provider for one request.\n///\n/// # Details\n///\n/// This wrapper preserves the service contract: it blocks, closes child input, and returns complete\n/// owned captures. It requires exclusive child-process and allocator providers.\npub effect fn submit(\n request: &ProcessRequest\n) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut ChildProcess | &mut Allocator {\n return run ChildProcess.execute(request)\n}\n", }, { module: 'silk/effect', path: 'silk/effect.silk', sourceIdentity: 'silk/effect', - digest: 'ac84781c5db88a646cb1153e22f26a4464fd4bedaad30ab5b774d648f10da0a4', + digest: '9740e2cba0147417dac21f4e896fbc33d3413f47ebef4f7106258a53019bdb46', documentation: 'silk/effect.silk', layer: 'portable', runtimeInventory: [ @@ -109,7 +109,7 @@ export const modules = [ ], namespace: 'Effect', source: - "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, Success, Failure }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result {value: outcome} => match move outcome {\n/// Result.Success {value} => value\n/// Result.Failure {error} => error.answer\n/// }\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n return run Intrinsic.effectResult(move protected)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => onSuccess(move success)\n Failure { error } => run raise(onFailure(move error))\n }\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let completed = run result(move self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => onSuccess(move success)\n Failure { error } => run raise(move error)\n }\n }\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => move success\n Failure { error } => run raise(onFailure(move error))\n }\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => run onSuccess(move success)\n Failure { error } => run raise(move error)\n }\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => run callback(move success)\n Failure { error } => run raise(move error)\n }\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => move success\n Failure { error } => run onFailure(move error)\n }\n }\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => move success\n Failure { error } => run raise(move error)\n }\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => move success\n Failure { error } => run retryFailure(self, move error, retries)\n }\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => move success\n Failure { error } => run raise(move error)\n }\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", + "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n return run Intrinsic.effectResult(move protected)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run onFailure(move error)\n }\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", }, { module: 'silk/execution', @@ -256,7 +256,7 @@ export const modules = [ module: 'silk/filesystem', path: 'silk/filesystem.silk', sourceIdentity: 'silk/filesystem', - digest: '3a5f9fdeffb990e1cc3585e791bd3f54860be008f8ecf7044809f81254b8a2bc', + digest: '371a54137c198d30d24760811a42596e887a055805160213b6f8166fa1c70c86', documentation: 'silk/filesystem.silk', layer: 'portable', runtimeInventory: ['effectResult', 'replace', 'stringFromUtf8Unchecked'], @@ -272,20 +272,20 @@ export const modules = [ 'Path', ], source: - '//! Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.\n//!\n//! # When to use\n//! Build provider-absolute [`Path`] values with [`make`] or [`fromBytes`], then run operations\n//! through a supplied [`FileSystem`]. Use [`rawBytes`] for platform values that must round-trip even\n//! when they are not UTF-8, and [`resolve`] for lexical relative-path resolution.\n//!\n//! # Details\n//! Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and\n//! embedded `.` or `..`. Resolution handles relative dot components but rejects escape above root.\n//! Directory listings return independently owned child paths in deterministic path-byte order.\n//! Portable [`FileError`] data names both the operation and a closed recovery reason, with an\n//! optional provider code for diagnostics.\n//!\n//! Temporary directories have an explicit lifecycle because removal can fail and needs services.\n//! Use [`release`] when cleanup failure matters, or [`releaseIgnored`] as an infallible finalizer\n//! only after deliberately accepting that loss.\n//!\n//! # Gotchas\n//! A path created from arbitrary bytes may not have a valid text view. Keep using [`rawBytes`] unless\n//! the bytes were validated as UTF-8; [`view`] and [`name`] rely on that caller knowledge.\n//!\n//! # Examples\n//! ## Construct and inspect a portable path\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as FileSystem\n//!\n//! effect fn example() -> i32\n//! ! FileSystem.FileError | Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let path = run FileSystem.make("/workspace")\n//! |> Effect.provideMut(&mut allocator)\n//! if FileSystem.name(&path) == "workspace" {\n//! return 42\n//! }\n//! return 0\n//! }\n//!\n//! effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(example(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n append as bytesAppend,\n asSlice as bytesAsSlice\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.option { None, Option, Some, none, some }\nimport silk.result { Failure, Result, Success }\nimport silk.string {\n InvalidUtf8,\n fromUtf8 as stringFromUtf8,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n length as vectorLength,\n make as vectorMake,\n pop as vectorPop\n}\n\n/// An owned, normalized absolute path in a [`FileSystem`] provider\'s portable namespace.\n///\n/// # Details\n///\n/// Portable `/` means the selected provider\'s root, not necessarily the host operating system\'s\n/// root. Construct paths through [`make`], [`fromBytes`], [`root`], [`join`], or [`resolve`]; the\n/// representation is private so every `Path` satisfies the normalization rules.\npub struct Path {\n bytes: Bytes\n nameBytes: Bytes\n}\n\n/// Minimal portable metadata for one regular file.\npub struct FileInfo {\n /// Complete file length in bytes.\n pub byteLength: usize\n}\n\n/// Portable metadata identifying a directory; no platform-specific fields are exposed.\npub struct DirectoryInfo {}\n\n/// The closed portable kind of one directory entry.\npub struct DirectoryEntryKind {\n /// Stable portable kind code selected by [`file`] or [`directory`].\n pub code: i32\n}\n\n/// One immediate directory child with an independently owned complete [`Path`].\npub struct DirectoryEntry {\n /// Independently owned complete path to the child.\n pub path: Path\n /// Portable kind reported for the child.\n pub kind: DirectoryEntryKind\n}\n\n/// The stable portable operation category stored in a [`FileError`].\npub struct FileOperation {\n /// Stable code identifying the attempted portable operation.\n pub code: i32\n}\n\n/// A stable portable recovery category stored in a [`FileError`].\npub struct FileReason {\n /// Stable code identifying the portable recovery reason.\n pub code: i32\n}\n\n/// An allocation-free portable failure naming the attempted operation and recovery reason.\n///\n/// # Details\n///\n/// Match or compare [`operationCode`] and [`reasonCode`] for portable recovery. [`providerCode`]\n/// may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions\n/// must not depend on it.\npub struct FileError {\n /// The operation that failed.\n pub operation: FileOperation\n /// The portable reason callers can recover by.\n pub reason: FileReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Constructs the regular-file [`DirectoryEntryKind`].\npub fn file() -> DirectoryEntryKind { return DirectoryEntryKind { code: 0 } }\n\n/// Constructs the directory [`DirectoryEntryKind`].\npub fn directory() -> DirectoryEntryKind { return DirectoryEntryKind { code: 1 } }\n\n/// Returns the stable code for a consumed [`DirectoryEntryKind`]: `0` for file, `1` for directory.\npub fn entryKindCode(kind: DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Reads the stable directory-entry kind code through a borrow.\nfn borrowedKindCode(kind: &DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Constructs regular-file metadata with the complete length in bytes.\npub fn fileInfo(byteLength: usize) -> FileInfo {\n return FileInfo { byteLength: byteLength }\n}\n\n/// Constructs the fieldless portable directory metadata value.\npub fn directoryInfo() -> DirectoryInfo { return DirectoryInfo {} }\n\n/// Constructs a directory entry by taking ownership of its complete child `path` and `kind`.\npub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntry {\n return DirectoryEntry { path: move path, kind: move kind }\n}\n\n/// Selects the read-file operation.\npub fn readFileOperation() -> FileOperation { return FileOperation { code: 0 } }\n\n/// Selects the write-file operation.\npub fn writeFileOperation() -> FileOperation { return FileOperation { code: 1 } }\n\n/// Selects the stat operation.\npub fn statOperation() -> FileOperation { return FileOperation { code: 2 } }\n\n/// Selects the list-directory operation.\npub fn listDirectoryOperation() -> FileOperation { return FileOperation { code: 3 } }\n\n/// Selects the create-directory operation.\npub fn createDirectoryOperation() -> FileOperation { return FileOperation { code: 4 } }\n\n/// Selects the remove-file operation.\npub fn removeFileOperation() -> FileOperation { return FileOperation { code: 5 } }\n\n/// Selects the remove-directory operation.\npub fn removeDirectoryOperation() -> FileOperation { return FileOperation { code: 6 } }\n\n/// Selects path construction and resolution.\npub fn pathOperation() -> FileOperation { return FileOperation { code: 7 } }\n\n/// Selects the create-temporary-directory operation.\npub fn createTemporaryDirectoryOperation() -> FileOperation { return FileOperation { code: 8 } }\n\n/// Returns the stable numeric code of a consumed [`FileOperation`].\npub fn operationCode(operation: FileOperation) -> i32 { return operation.code }\n\n/// Constructs the `NotFound` recovery reason.\npub fn notFound() -> FileReason { return FileReason { code: 0 } }\n\n/// Constructs the `AlreadyExists` recovery reason.\npub fn alreadyExists() -> FileReason { return FileReason { code: 1 } }\n\n/// Constructs the `PermissionDenied` recovery reason.\npub fn permissionDenied() -> FileReason { return FileReason { code: 2 } }\n\n/// Constructs the `InvalidPath` recovery reason.\npub fn invalidPath() -> FileReason { return FileReason { code: 3 } }\n\n/// Constructs the `WrongType` recovery reason.\npub fn wrongType() -> FileReason { return FileReason { code: 4 } }\n\n/// Constructs the `NotEmpty` recovery reason.\npub fn notEmpty() -> FileReason { return FileReason { code: 5 } }\n\n/// Constructs the `NoSpace` recovery reason.\npub fn noSpace() -> FileReason { return FileReason { code: 6 } }\n\n/// Constructs the `TooLarge` recovery reason.\npub fn tooLarge() -> FileReason { return FileReason { code: 7 } }\n\n/// Constructs the `Unsupported` recovery reason.\npub fn unsupported() -> FileReason { return FileReason { code: 8 } }\n\n/// Constructs the catch-all `Other` recovery reason.\npub fn other() -> FileReason { return FileReason { code: 9 } }\n\n/// Returns the stable numeric code of a consumed [`FileReason`].\npub fn reasonCode(reason: FileReason) -> i32 { return reason.code }\n\n/// Constructs a portable [`FileError`] without a provider-specific numeric detail.\npub fn error(operation: FileOperation, reason: FileReason) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false,\n }\n}\n\n/// Constructs a portable [`FileError`] while retaining one provider-specific diagnostic code.\n///\n/// # Details\n///\n/// The numeric `code` is opaque outside that provider. The portable `operation` and `reason` remain\n/// the fields callers should use for recovery.\npub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true,\n }\n}\n\n/// Borrows an error and returns its provider-specific numeric detail, if one was retained.\npub fn providerCode(error: &FileError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\neffect fn raise(error: FileError) -> never ! FileError { fail move error }\n\neffect fn rejectPath() -> never ! FileError {\n fail error(pathOperation(), invalidPath())\n}\n\nfn byte(value: u8) -> i32 { return u8.toI32(value) }\n\nfn containsNul(values: &[u8]) -> bool {\n let mut index = usize.ZERO\n while index < values.length {\n if values[index] == u8.toU8(0) { return true }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validUtf8(values: &[u8]) -> bool {\n let decoded = stringFromUtf8(values)\n return match move decoded {\n Result { value: outcome } => match move outcome {\n Success { value: text } => true\n Failure { error: invalid } => false\n }\n }\n}\n\nfn isDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != usize.ONE { return false }\n return byte(values[start]) == 46\n}\n\nfn isDotDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != 2 { return false }\n if byte(values[start]) != 46 { return false }\n return byte(values[start + usize.ONE]) == 46\n}\n\nfn validAbsolute(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) != 47 { return false }\n if containsNul(values) { return false }\n if values.length == usize.ONE { return true }\n let mut start = usize.ONE\n let mut index = usize.ONE\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validRelativeFragment(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) == 47 { return false }\n if containsNul(values) { return false }\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\neffect fn appendRange(\n target: Bytes,\n source: &[u8],\n start: usize,\n end: usize\n) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = move target\n let mut index = start\n while index < end {\n let one = [source[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\nfn finalNameStart(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ONE }\n let mut index = values.length\n while usize.ZERO < index {\n index = index - usize.ONE\n if byte(values[index]) == 47 { return index + usize.ONE }\n }\n return usize.ZERO\n}\n\neffect fn finishPath(bytes: Bytes) -> Path ! OutOfMemoryError ? &mut Allocator {\n let view = bytesAsSlice(&bytes)\n let start = finalNameStart(view)\n let nameBytes = run appendRange(bytesMake(), view, start, view.length)\n return Path { bytes: move bytes, nameBytes: move nameBytes }\n}\n\n/// Copies UTF-8 text into an owned, normalized provider-absolute [`Path`].\n///\n/// # Details\n///\n/// The text must begin with `/`. Root is valid; every other path must have nonempty components and\n/// no trailing slash, NUL, `.` component, or `..` component. Invalid input fails with\n/// `FileError(pathOperation(), invalidPath())`; copying can fail with [`OutOfMemoryError`].\npub effect fn make(value: string) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let values = stringUtf8Bytes(value)\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Constructs an owned normalized provider-absolute Path from exact platform bytes.\n///\n/// # Details\n///\n/// Platform paths are byte sequences, and a caller that received one from the platform — a\n/// directory entry, an argument, an environment value — must be able to hand it back unchanged.\n/// The same normalization applies as for textual construction: the value is absolute, rejects NUL,\n/// and rejects `.`, `..`, empty components, and trailing separators. Well-formed text is not\n/// required, so a Path built this way may have no `string` view.\npub effect fn fromBytes(values: &[u8]) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Allocates the portable root path `/` in the selected allocator.\npub effect fn root() -> Path ! OutOfMemoryError ? &mut Allocator {\n let mut copied = bytesMake()\n let appended = run bytesAppend(&mut copied, stringUtf8Bytes("/"))\n return run finishPath(move copied)\n}\n\nfn pathBytes(self: &Path) -> &[u8] { return bytesAsSlice(&self.bytes) }\n\n/// Borrows the complete normalized path as exact platform bytes.\n///\n/// # Details\n///\n/// This is the lossless view. It round-trips a Path built from platform bytes even when those\n/// bytes are not well-formed text, which the `string` view cannot promise.\npub fn rawBytes(self: &Path) -> &[u8] {\n return pathBytes(self)\n}\n\n/// Borrows the complete path as text when its bytes are known to be valid UTF-8.\n///\n/// # Details\n///\n/// Paths from [`make`], [`join`], [`joinUtf8`], and [`resolve`] satisfy that precondition. A path\n/// created with [`fromBytes`] may not; use [`rawBytes`] unless the source bytes were validated.\npub fn view(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(pathBytes(self)) }\n return ""\n}\n\n/// Returns `true` exactly when this path is the portable root `/`.\npub fn isRoot(self: &Path) -> bool { return bytesAsSlice(&self.bytes).length == usize.ONE }\n\n/// Borrows the final component as text; root returns empty text.\n///\n/// # Details\n///\n/// This has the same UTF-8 precondition as [`view`]. It does not allocate or include a separator.\npub fn name(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(bytesAsSlice(&self.nameBytes)) }\n return ""\n}\n\neffect fn joinBytes(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validRelativeFragment(fragment) == false { return run rejectPath() }\n let baseBytes = pathBytes(base)\n let mut combined = run appendRange(bytesMake(), baseBytes, usize.ZERO, baseBytes.length)\n if isRoot(base) == false {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeChild = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeChild, fragment, usize.ZERO, fragment.length)\n return run finishPath(move combined)\n}\n\n/// Appends one normalized relative text fragment to an absolute base path.\n///\n/// # Details\n///\n/// `fragment` must be nonempty and relative, with no NUL, empty, `.`, or `..` component and no\n/// trailing slash. Use [`resolve`] when dot components should be interpreted instead of rejected.\npub effect fn join(\n base: &Path,\n fragment: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return run joinBytes(base, stringUtf8Bytes(fragment))\n}\n\n/// Validates UTF-8 bytes as one normalized relative fragment and appends them to `base`.\n///\n/// # Details\n///\n/// This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the\n/// same malformed components rejected by [`join`] fail with the `InvalidPath` reason.\npub effect fn joinUtf8(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validUtf8(fragment) == false { return run rejectPath() }\n return run joinBytes(base, fragment)\n}\n\nfn componentCount(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ZERO }\n let mut count = usize.ONE\n let mut index = usize.ONE\n while index < values.length {\n if byte(values[index]) == 47 { count = count + usize.ONE }\n index = index + usize.ONE\n }\n return count\n}\n\nfn survivingRelative(values: &[u8], after: usize) -> bool {\n let mut depth = usize.ONE\n let mut start = after\n let mut index = after\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDotDot(values, start, index) {\n depth = depth - usize.ONE\n if depth == usize.ZERO { return false }\n } else {\n if isDot(values, start, index) == false { depth = depth + usize.ONE }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return true\n}\n\n/// Resolves relative text lexically against an explicit absolute base.\n///\n/// # Details\n///\n/// Empty text and `.` keep the base; `..` removes components; ordinary components append. An\n/// absolute relative value, an empty interior component, NUL, or any attempt to escape above root\n/// fails with the `InvalidPath` reason. Resolution is lexical and never accesses the filesystem.\npub effect fn resolve(\n base: &Path,\n relativeText: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let relative = stringUtf8Bytes(relativeText)\n if containsNul(relative) { return run rejectPath() }\n if usize.ZERO < relative.length {\n if byte(relative[usize.ZERO]) == 47 { return run rejectPath() }\n }\n let baseBytes = pathBytes(base)\n let mut keptBase = componentCount(baseBytes)\n let mut relativeDepth = usize.ZERO\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start == index {\n if index != relative.length { return run rejectPath() }\n } else {\n if isDotDot(relative, start, index) {\n if usize.ZERO < relativeDepth {\n relativeDepth = relativeDepth - usize.ONE\n } else {\n if keptBase == usize.ZERO { return run rejectPath() }\n keptBase = keptBase - usize.ONE\n }\n } else {\n if isDot(relative, start, index) == false {\n relativeDepth = relativeDepth + usize.ONE\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n let mut combined = bytesMake()\n let rooted = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n let mut included = usize.ZERO\n start = usize.ONE\n index = usize.ONE\n while index <= baseBytes.length {\n let mut boundary = false\n if index == baseBytes.length {\n boundary = true\n } else {\n if byte(baseBytes[index]) == 47 { boundary = true }\n }\n if boundary {\n if included < keptBase {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeBaseComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeBaseComponent, baseBytes, start, index)\n included = included + usize.ONE\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n start = usize.ZERO\n index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDot(relative, start, index) == false {\n if isDotDot(relative, start, index) == false {\n if survivingRelative(relative, index + usize.ONE) {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeRelativeComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeRelativeComponent, relative, start, index)\n included = included + usize.ONE\n }\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return run finishPath(move combined)\n}\n\n/// Allocates an independently owned parent path, or [`None`] when `self` is root.\n///\n/// # Details\n///\n/// The result does not borrow `self`. A direct child of root has root as its parent.\npub effect fn parent(\n self: &Path\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n if isRoot(self) { return none() }\n let values = pathBytes(self)\n let nameStart = finalNameStart(values)\n let mut end = usize.ONE\n if nameStart != usize.ONE { end = nameStart - usize.ONE }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, end)\n let owned = run finishPath(move copied)\n return some(move owned)\n}\n\n/// Portable mutable service for normalized paths and whole-file operations.\n///\n/// # Details\n///\n/// Application code supplies one provider lexically with `Effect.provideMut`; tests can implement\n/// this service in memory, while native applications can use `silk.os_filesystem`. The service owns\n/// platform policy, but every implementation must preserve the portable error categories,\n/// create-or-truncate writes, and deterministic listing order described here.\n///\n/// # Examples\n/// ## Write a file after creating its parents\n/// ```silk\n/// import silk.allocator { Allocator }\n///\n/// import silk.filesystem as FileSystem\n///\n/// import silk.usize as usize\n///\n/// pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize\n/// ! FileSystem.FileError | Allocator.OutOfMemoryError\n/// ? &mut FileSystem.FileSystem | &mut Allocator {\n/// let written = run FileSystem.writeFileWithParents(path, contents)\n/// return contents.length\n/// }\n/// ```\npub service FileSystem {\n /// Reads one complete regular file into independently owned bytes.\n ///\n /// # Details\n ///\n /// Reading a directory fails with `WrongType`. Allocation of the returned [`Bytes`] may fail\n /// independently of the provider read.\n effect fn readFile(\n path: &Path\n ) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Writes one complete byte view with create-or-truncate semantics.\n ///\n /// # Details\n ///\n /// A missing file is created; an existing regular file is replaced by exactly `bytes`. The call\n /// does not create missing parent directories—use [`writeFileWithParents`] for that workflow.\n effect fn writeFile(path: &Path, bytes: &[u8]) -> () ! FileError ? &mut FileSystem\n /// Returns [`FileInfo`] or [`DirectoryInfo`] for the path without opening file contents.\n ///\n /// # Details\n ///\n /// Missing paths fail with `NotFound`; providers use `WrongType` only when an operation requires a\n /// particular kind, not for this discriminating query.\n effect fn stat(path: &Path) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem\n /// Returns immediate owned children in deterministic complete-path byte order.\n ///\n /// # Details\n ///\n /// The result is not recursive. Each `DirectoryEntry.path` is independently owned and may be\n /// retained after the listing vector is released.\n effect fn listDirectory(\n path: &Path\n ) -> Vector ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Creates exactly one missing directory whose parent already exists.\n ///\n /// # Details\n ///\n /// Existing paths fail with `AlreadyExists`; use [`createDirectoriesRecursively`] to ensure every\n /// missing component.\n effect fn createDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one regular file and fails with `WrongType` for a directory.\n effect fn removeFile(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one empty directory.\n ///\n /// # Details\n ///\n /// A nonempty directory fails with `NotEmpty`; use [`removeDirectoryRecursively`] only when all\n /// descendants are intentionally in scope for removal.\n effect fn removeDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Creates one directory under an existing parent under a name no other caller holds.\n ///\n /// # Details\n ///\n /// The provider chooses the name\'s unique part and returns the complete Path, because only the\n /// provider can create and claim a name in one step. A caller that supplied the name would have\n /// to check-then-create, and the gap between those two is exactly the race this avoids.\n /// `prefix` is a byte prefix for the provider-chosen child name, not a complete path. The returned\n /// directory already exists and is an immediate child of `parent`.\n effect fn createTemporaryDirectory(\n parent: &Path,\n prefix: &[u8]\n ) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n}\n\n/// A directory a caller owns outright, together with everything written inside it.\n///\n/// # Details\n///\n/// Ownership is affine: `TemporaryDirectory` holds an owned `Path`, so exactly one binding holds\n/// it and the compiler rejects a second use of a moved one. Ownership is not, however, a `Drop`\n/// hook. Removing a directory is a fallible operation that requires the `FileSystem` capability,\n/// and a `Drop` hook may carry neither a failure row nor a requirement row, so a hook here could\n/// only be written by inventing an infallible intrinsic over a fallible syscall. Release is\n/// therefore explicit and honest about both rows — see `release`.\n///\n/// Scope ownership comes from composition rather than from a hook: `Effect.ensuring(release)`\n/// runs the release whatever the protected Effect\'s outcome. Because `ensuring` types its\n/// finalizer `! never`, that composition has to say what a failed removal means; `releaseIgnored`\n/// is the stdlib\'s answer and names the loss at the call site.\npub struct TemporaryDirectory {\n /// The complete owned path callers use while the scope remains live.\n pub path: Path\n}\n\n/// Creates an explicitly owned temporary directory under `parent` with a name beginning in `prefix`.\n///\n/// # Details\n///\n/// The result is owned. Nothing removes it until a caller runs [`release`] or [`releaseIgnored`].\n/// The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in\n/// one operation.\npub effect fn temporaryDirectory(\n parent: &Path,\n prefix: string\n) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let created = run FileSystem.createTemporaryDirectory(parent, stringUtf8Bytes(prefix))\n return TemporaryDirectory { path: move created }\n}\n\n/// Consumes one TemporaryDirectory and removes it together with everything inside it.\n///\n/// # Details\n///\n/// Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the\n/// tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a\n/// failed cleanup uses this operation and handles the failure. The owner is consumed even when\n/// removal fails, so copy any diagnostic path information needed before calling.\npub effect fn release(\n self: TemporaryDirectory\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let owned = move self\n let removed = run removeDirectoryRecursively(&owned.path)\n drop owned\n return ()\n}\n\neffect fn discardReleaseFailure(error: FileError | OutOfMemoryError) -> () { return () }\n\n/// Consumes one TemporaryDirectory, removes it, and discards a failed removal.\n///\n/// # Details\n///\n/// This exists because `Effect.ensuring` types its finalizer `! never`, so a fallible release has\n/// to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a\n/// caller reading `releaseIgnored` at the call site can see that a failed removal is being\n/// dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —\n/// a directory the host will reap — and what is kept is the protected Effect\'s own outcome, which\n/// is the answer the program was computing.\n///\n/// A caller who needs the failure uses `release` instead and does not compose it with `ensuring`.\n///\n/// The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer\n/// starts. Derive the required paths before you give the owner to the finalizer.\npub effect fn releaseIgnored(\n self: TemporaryDirectory\n) -> () ? &mut FileSystem | &mut Allocator {\n return run Effect.catchAll(release(move self), discardReleaseFailure)\n}\n\n/// Copies one recorded Path out of the walk\'s own record.\n///\n/// The walk appends to the same record it is reading, so it reads through a copy rather than\n/// through a borrow that the next append would invalidate.\neffect fn recordedCopy(\n recorded: &Vector,\n index: usize\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return match &vectorAsSlice(recorded)[index] {\n Path { bytes, nameBytes } => run fromBytes(bytesAsSlice(&bytes))\n }\n}\n\n/// Removes a directory, every descendant file, and every descendant directory.\n///\n/// # Details\n///\n/// Two passes, because the portable primitive removes exactly one *empty* directory. The first\n/// pass walks the tree front to back, unlinking every file it meets and recording every directory\n/// it meets; the second removes the recorded directories back to front. That order is\n/// child-before-parent for free: a directory is always recorded before the children found inside\n/// it, so reversing the record reverses the containment. Neither pass recurses, so depth costs\n/// vector capacity rather than stack.\n///\n/// This operation is destructive and not transactional. If a provider or allocation failure occurs,\n/// removals already completed remain completed and the remaining tree is left in place.\npub effect fn removeDirectoryRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let mut recorded = vectorMake()\n let seed = run fromBytes(rawBytes(path))\n let noted = run vectorAppend(&mut recorded, move seed)\n let mut index = usize.ZERO\n while index < vectorLength(&recorded) {\n let current = run recordedCopy(&recorded, index)\n let entries = run FileSystem.listDirectory(¤t)\n let listed = vectorAsSlice(&entries)\n let mut cursor = usize.ZERO\n while cursor < listed.length {\n let childKind = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } => borrowedKindCode(&childEntryKind)\n }\n if childKind == 0 {\n let unlinked = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run FileSystem.removeFile(&childPath)\n }\n } else {\n let toRemove = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run fromBytes(rawBytes(&childPath))\n }\n let notedChild = run vectorAppend(&mut recorded, move toRemove)\n }\n cursor = cursor + usize.ONE\n }\n index = index + usize.ONE\n }\n while usize.ZERO < vectorLength(&recorded) {\n let taken = vectorPop(&mut recorded)\n let emptied = match move taken {\n Some { value: selected } => move selected\n None {} => run fromBytes(rawBytes(path))\n }\n let removed = run FileSystem.removeDirectory(&emptied)\n }\n return ()\n}\n\nstruct DirectoryPresent {}\nstruct DirectoryMissing {}\nstruct DirectoryWrongType {}\nstruct DirectoryStatFailure { error: FileError }\n\nfn classifyStatFailure(\n failure: FileError\n) -> DirectoryMissing | DirectoryStatFailure {\n if failure.reason.code == 0 { return DirectoryMissing {} }\n return DirectoryStatFailure { error: move failure }\n}\n\nfn classifyDirectory(\n outcome: Success | Failure\n) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure {\n return match move outcome {\n Success { value: info } => match move info {\n DirectoryInfo {} => DirectoryPresent {}\n FileInfo { byteLength } => DirectoryWrongType {}\n }\n Failure { error: failure } => classifyStatFailure(move failure)\n }\n}\n\n/// Ensures that `path` and every missing ancestor exist as directories.\n///\n/// # Details\n///\n/// Existing directories are kept. An existing regular file at any component fails with\n/// `WrongType`; failures other than `NotFound` propagate. This is ordinary stat-then-create\n/// composition, so concurrent namespace changes may still race according to provider policy.\npub effect fn createDirectoriesRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let values = pathBytes(path)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix))\n let decision = match move completed {\n Result { value: outcome } =>\n classifyDirectory(move outcome)\n }\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return ()\n}\n\n/// Ensures every parent directory exists, then writes the complete byte view to `path`.\n///\n/// # Details\n///\n/// The final write uses `FileSystem.writeFile` create-or-truncate semantics. Passing root delegates\n/// directly to the provider and normally fails with `WrongType`. Directory creation and writing are\n/// not transactional, so a later failure may leave newly created parents behind.\npub effect fn writeFileWithParents(\n path: &Path,\n bytes: &[u8]\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n if isRoot(path) { return run FileSystem.writeFile(path, bytes) }\n let pathValues = pathBytes(path)\n let nameStart = finalNameStart(pathValues)\n let mut parentEnd = usize.ONE\n if nameStart != usize.ONE { parentEnd = nameStart - usize.ONE }\n let parentBytes = run appendRange(bytesMake(), pathValues, usize.ZERO, parentEnd)\n let ownedParent = run finishPath(move parentBytes)\n let values = pathBytes(&ownedParent)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix))\n let decision = match move completed {\n Result { value: outcome } =>\n classifyDirectory(move outcome)\n }\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return run FileSystem.writeFile(path, bytes)\n}\n\neffect fn existsFailure(failure: FileError) -> bool ! FileError {\n if failure.reason.code == 0 { return false }\n return run raise(move failure)\n}\n\n/// Returns whether a file or directory exists at `path`.\n///\n/// # Details\n///\n/// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider\n/// failure propagate so callers cannot mistake an inaccessible path for an absent one.\npub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem {\n let completed = run Intrinsic.effectResult(FileSystem.stat(path))\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: info } => true\n Failure { error: failure } => run existsFailure(move failure)\n }\n }\n}\n', + '//! Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.\n//!\n//! # When to use\n//! Build provider-absolute [`Path`] values with [`make`] or [`fromBytes`], then run operations\n//! through a supplied [`FileSystem`]. Use [`rawBytes`] for platform values that must round-trip even\n//! when they are not UTF-8, and [`resolve`] for lexical relative-path resolution.\n//!\n//! # Details\n//! Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and\n//! embedded `.` or `..`. Resolution handles relative dot components but rejects escape above root.\n//! Directory listings return independently owned child paths in deterministic path-byte order.\n//! Portable [`FileError`] data names both the operation and a closed recovery reason, with an\n//! optional provider code for diagnostics.\n//!\n//! Temporary directories have an explicit lifecycle because removal can fail and needs services.\n//! Use [`release`] when cleanup failure matters, or [`releaseIgnored`] as an infallible finalizer\n//! only after deliberately accepting that loss.\n//!\n//! # Gotchas\n//! A path created from arbitrary bytes may not have a valid text view. Keep using [`rawBytes`] unless\n//! the bytes were validated as UTF-8; [`view`] and [`name`] rely on that caller knowledge.\n//!\n//! # Examples\n//! ## Construct and inspect a portable path\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as FileSystem\n//!\n//! effect fn example() -> i32\n//! ! FileSystem.FileError | Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let path = run FileSystem.make("/workspace")\n//! |> Effect.provideMut(&mut allocator)\n//! if FileSystem.name(&path) == "workspace" {\n//! return 42\n//! }\n//! return 0\n//! }\n//!\n//! effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(example(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n append as bytesAppend,\n asSlice as bytesAsSlice\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string {\n InvalidUtf8,\n fromUtf8 as stringFromUtf8,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n length as vectorLength,\n make as vectorMake,\n pop as vectorPop\n}\n\n/// An owned, normalized absolute path in a [`FileSystem`] provider\'s portable namespace.\n///\n/// # Details\n///\n/// Portable `/` means the selected provider\'s root, not necessarily the host operating system\'s\n/// root. Construct paths through [`make`], [`fromBytes`], [`root`], [`join`], or [`resolve`]; the\n/// representation is private so every `Path` satisfies the normalization rules.\npub struct Path {\n bytes: Bytes\n nameBytes: Bytes\n}\n\n/// Minimal portable metadata for one regular file.\npub struct FileInfo {\n /// Complete file length in bytes.\n pub byteLength: usize\n}\n\n/// Portable metadata identifying a directory; no platform-specific fields are exposed.\npub struct DirectoryInfo {}\n\n/// The closed portable kind of one directory entry.\npub struct DirectoryEntryKind {\n /// Stable portable kind code selected by [`file`] or [`directory`].\n pub code: i32\n}\n\n/// One immediate directory child with an independently owned complete [`Path`].\npub struct DirectoryEntry {\n /// Independently owned complete path to the child.\n pub path: Path\n /// Portable kind reported for the child.\n pub kind: DirectoryEntryKind\n}\n\n/// The stable portable operation category stored in a [`FileError`].\npub struct FileOperation {\n /// Stable code identifying the attempted portable operation.\n pub code: i32\n}\n\n/// A stable portable recovery category stored in a [`FileError`].\npub struct FileReason {\n /// Stable code identifying the portable recovery reason.\n pub code: i32\n}\n\n/// An allocation-free portable failure naming the attempted operation and recovery reason.\n///\n/// # Details\n///\n/// Match or compare [`operationCode`] and [`reasonCode`] for portable recovery. [`providerCode`]\n/// may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions\n/// must not depend on it.\npub struct FileError {\n /// The operation that failed.\n pub operation: FileOperation\n /// The portable reason callers can recover by.\n pub reason: FileReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Constructs the regular-file [`DirectoryEntryKind`].\npub fn file() -> DirectoryEntryKind { return DirectoryEntryKind { code: 0 } }\n\n/// Constructs the directory [`DirectoryEntryKind`].\npub fn directory() -> DirectoryEntryKind { return DirectoryEntryKind { code: 1 } }\n\n/// Returns the stable code for a consumed [`DirectoryEntryKind`]: `0` for file, `1` for directory.\npub fn entryKindCode(kind: DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Reads the stable directory-entry kind code through a borrow.\nfn borrowedKindCode(kind: &DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Constructs regular-file metadata with the complete length in bytes.\npub fn fileInfo(byteLength: usize) -> FileInfo {\n return FileInfo { byteLength: byteLength }\n}\n\n/// Constructs the fieldless portable directory metadata value.\npub fn directoryInfo() -> DirectoryInfo { return DirectoryInfo {} }\n\n/// Constructs a directory entry by taking ownership of its complete child `path` and `kind`.\npub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntry {\n return DirectoryEntry { path: move path, kind: move kind }\n}\n\n/// Selects the read-file operation.\npub fn readFileOperation() -> FileOperation { return FileOperation { code: 0 } }\n\n/// Selects the write-file operation.\npub fn writeFileOperation() -> FileOperation { return FileOperation { code: 1 } }\n\n/// Selects the stat operation.\npub fn statOperation() -> FileOperation { return FileOperation { code: 2 } }\n\n/// Selects the list-directory operation.\npub fn listDirectoryOperation() -> FileOperation { return FileOperation { code: 3 } }\n\n/// Selects the create-directory operation.\npub fn createDirectoryOperation() -> FileOperation { return FileOperation { code: 4 } }\n\n/// Selects the remove-file operation.\npub fn removeFileOperation() -> FileOperation { return FileOperation { code: 5 } }\n\n/// Selects the remove-directory operation.\npub fn removeDirectoryOperation() -> FileOperation { return FileOperation { code: 6 } }\n\n/// Selects path construction and resolution.\npub fn pathOperation() -> FileOperation { return FileOperation { code: 7 } }\n\n/// Selects the create-temporary-directory operation.\npub fn createTemporaryDirectoryOperation() -> FileOperation { return FileOperation { code: 8 } }\n\n/// Returns the stable numeric code of a consumed [`FileOperation`].\npub fn operationCode(operation: FileOperation) -> i32 { return operation.code }\n\n/// Constructs the `NotFound` recovery reason.\npub fn notFound() -> FileReason { return FileReason { code: 0 } }\n\n/// Constructs the `AlreadyExists` recovery reason.\npub fn alreadyExists() -> FileReason { return FileReason { code: 1 } }\n\n/// Constructs the `PermissionDenied` recovery reason.\npub fn permissionDenied() -> FileReason { return FileReason { code: 2 } }\n\n/// Constructs the `InvalidPath` recovery reason.\npub fn invalidPath() -> FileReason { return FileReason { code: 3 } }\n\n/// Constructs the `WrongType` recovery reason.\npub fn wrongType() -> FileReason { return FileReason { code: 4 } }\n\n/// Constructs the `NotEmpty` recovery reason.\npub fn notEmpty() -> FileReason { return FileReason { code: 5 } }\n\n/// Constructs the `NoSpace` recovery reason.\npub fn noSpace() -> FileReason { return FileReason { code: 6 } }\n\n/// Constructs the `TooLarge` recovery reason.\npub fn tooLarge() -> FileReason { return FileReason { code: 7 } }\n\n/// Constructs the `Unsupported` recovery reason.\npub fn unsupported() -> FileReason { return FileReason { code: 8 } }\n\n/// Constructs the catch-all `Other` recovery reason.\npub fn other() -> FileReason { return FileReason { code: 9 } }\n\n/// Returns the stable numeric code of a consumed [`FileReason`].\npub fn reasonCode(reason: FileReason) -> i32 { return reason.code }\n\n/// Constructs a portable [`FileError`] without a provider-specific numeric detail.\npub fn error(operation: FileOperation, reason: FileReason) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false,\n }\n}\n\n/// Constructs a portable [`FileError`] while retaining one provider-specific diagnostic code.\n///\n/// # Details\n///\n/// The numeric `code` is opaque outside that provider. The portable `operation` and `reason` remain\n/// the fields callers should use for recovery.\npub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true,\n }\n}\n\n/// Borrows an error and returns its provider-specific numeric detail, if one was retained.\npub fn providerCode(error: &FileError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\neffect fn raise(error: FileError) -> never ! FileError { fail move error }\n\neffect fn rejectPath() -> never ! FileError {\n fail error(pathOperation(), invalidPath())\n}\n\nfn byte(value: u8) -> i32 { return u8.toI32(value) }\n\nfn containsNul(values: &[u8]) -> bool {\n let mut index = usize.ZERO\n while index < values.length {\n if values[index] == u8.toU8(0) { return true }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validUtf8(values: &[u8]) -> bool {\n let decoded = stringFromUtf8(values)\n return match move decoded {\n Result.Success { value: text } => true\n Result.Failure { error: invalid } => false\n }\n}\n\nfn isDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != usize.ONE { return false }\n return byte(values[start]) == 46\n}\n\nfn isDotDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != 2 { return false }\n if byte(values[start]) != 46 { return false }\n return byte(values[start + usize.ONE]) == 46\n}\n\nfn validAbsolute(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) != 47 { return false }\n if containsNul(values) { return false }\n if values.length == usize.ONE { return true }\n let mut start = usize.ONE\n let mut index = usize.ONE\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validRelativeFragment(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) == 47 { return false }\n if containsNul(values) { return false }\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\neffect fn appendRange(\n target: Bytes,\n source: &[u8],\n start: usize,\n end: usize\n) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = move target\n let mut index = start\n while index < end {\n let one = [source[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\nfn finalNameStart(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ONE }\n let mut index = values.length\n while usize.ZERO < index {\n index = index - usize.ONE\n if byte(values[index]) == 47 { return index + usize.ONE }\n }\n return usize.ZERO\n}\n\neffect fn finishPath(bytes: Bytes) -> Path ! OutOfMemoryError ? &mut Allocator {\n let view = bytesAsSlice(&bytes)\n let start = finalNameStart(view)\n let nameBytes = run appendRange(bytesMake(), view, start, view.length)\n return Path { bytes: move bytes, nameBytes: move nameBytes }\n}\n\n/// Copies UTF-8 text into an owned, normalized provider-absolute [`Path`].\n///\n/// # Details\n///\n/// The text must begin with `/`. Root is valid; every other path must have nonempty components and\n/// no trailing slash, NUL, `.` component, or `..` component. Invalid input fails with\n/// `FileError(pathOperation(), invalidPath())`; copying can fail with [`OutOfMemoryError`].\npub effect fn make(value: string) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let values = stringUtf8Bytes(value)\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Constructs an owned normalized provider-absolute Path from exact platform bytes.\n///\n/// # Details\n///\n/// Platform paths are byte sequences, and a caller that received one from the platform — a\n/// directory entry, an argument, an environment value — must be able to hand it back unchanged.\n/// The same normalization applies as for textual construction: the value is absolute, rejects NUL,\n/// and rejects `.`, `..`, empty components, and trailing separators. Well-formed text is not\n/// required, so a Path built this way may have no `string` view.\npub effect fn fromBytes(values: &[u8]) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Allocates the portable root path `/` in the selected allocator.\npub effect fn root() -> Path ! OutOfMemoryError ? &mut Allocator {\n let mut copied = bytesMake()\n let appended = run bytesAppend(&mut copied, stringUtf8Bytes("/"))\n return run finishPath(move copied)\n}\n\nfn pathBytes(self: &Path) -> &[u8] { return bytesAsSlice(&self.bytes) }\n\n/// Borrows the complete normalized path as exact platform bytes.\n///\n/// # Details\n///\n/// This is the lossless view. It round-trips a Path built from platform bytes even when those\n/// bytes are not well-formed text, which the `string` view cannot promise.\npub fn rawBytes(self: &Path) -> &[u8] {\n return pathBytes(self)\n}\n\n/// Borrows the complete path as text when its bytes are known to be valid UTF-8.\n///\n/// # Details\n///\n/// Paths from [`make`], [`join`], [`joinUtf8`], and [`resolve`] satisfy that precondition. A path\n/// created with [`fromBytes`] may not; use [`rawBytes`] unless the source bytes were validated.\npub fn view(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(pathBytes(self)) }\n return ""\n}\n\n/// Returns `true` exactly when this path is the portable root `/`.\npub fn isRoot(self: &Path) -> bool { return bytesAsSlice(&self.bytes).length == usize.ONE }\n\n/// Borrows the final component as text; root returns empty text.\n///\n/// # Details\n///\n/// This has the same UTF-8 precondition as [`view`]. It does not allocate or include a separator.\npub fn name(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(bytesAsSlice(&self.nameBytes)) }\n return ""\n}\n\neffect fn joinBytes(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validRelativeFragment(fragment) == false { return run rejectPath() }\n let baseBytes = pathBytes(base)\n let mut combined = run appendRange(bytesMake(), baseBytes, usize.ZERO, baseBytes.length)\n if isRoot(base) == false {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeChild = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeChild, fragment, usize.ZERO, fragment.length)\n return run finishPath(move combined)\n}\n\n/// Appends one normalized relative text fragment to an absolute base path.\n///\n/// # Details\n///\n/// `fragment` must be nonempty and relative, with no NUL, empty, `.`, or `..` component and no\n/// trailing slash. Use [`resolve`] when dot components should be interpreted instead of rejected.\npub effect fn join(\n base: &Path,\n fragment: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return run joinBytes(base, stringUtf8Bytes(fragment))\n}\n\n/// Validates UTF-8 bytes as one normalized relative fragment and appends them to `base`.\n///\n/// # Details\n///\n/// This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the\n/// same malformed components rejected by [`join`] fail with the `InvalidPath` reason.\npub effect fn joinUtf8(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validUtf8(fragment) == false { return run rejectPath() }\n return run joinBytes(base, fragment)\n}\n\nfn componentCount(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ZERO }\n let mut count = usize.ONE\n let mut index = usize.ONE\n while index < values.length {\n if byte(values[index]) == 47 { count = count + usize.ONE }\n index = index + usize.ONE\n }\n return count\n}\n\nfn survivingRelative(values: &[u8], after: usize) -> bool {\n let mut depth = usize.ONE\n let mut start = after\n let mut index = after\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDotDot(values, start, index) {\n depth = depth - usize.ONE\n if depth == usize.ZERO { return false }\n } else {\n if isDot(values, start, index) == false { depth = depth + usize.ONE }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return true\n}\n\n/// Resolves relative text lexically against an explicit absolute base.\n///\n/// # Details\n///\n/// Empty text and `.` keep the base; `..` removes components; ordinary components append. An\n/// absolute relative value, an empty interior component, NUL, or any attempt to escape above root\n/// fails with the `InvalidPath` reason. Resolution is lexical and never accesses the filesystem.\npub effect fn resolve(\n base: &Path,\n relativeText: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let relative = stringUtf8Bytes(relativeText)\n if containsNul(relative) { return run rejectPath() }\n if usize.ZERO < relative.length {\n if byte(relative[usize.ZERO]) == 47 { return run rejectPath() }\n }\n let baseBytes = pathBytes(base)\n let mut keptBase = componentCount(baseBytes)\n let mut relativeDepth = usize.ZERO\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start == index {\n if index != relative.length { return run rejectPath() }\n } else {\n if isDotDot(relative, start, index) {\n if usize.ZERO < relativeDepth {\n relativeDepth = relativeDepth - usize.ONE\n } else {\n if keptBase == usize.ZERO { return run rejectPath() }\n keptBase = keptBase - usize.ONE\n }\n } else {\n if isDot(relative, start, index) == false {\n relativeDepth = relativeDepth + usize.ONE\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n let mut combined = bytesMake()\n let rooted = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n let mut included = usize.ZERO\n start = usize.ONE\n index = usize.ONE\n while index <= baseBytes.length {\n let mut boundary = false\n if index == baseBytes.length {\n boundary = true\n } else {\n if byte(baseBytes[index]) == 47 { boundary = true }\n }\n if boundary {\n if included < keptBase {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeBaseComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeBaseComponent, baseBytes, start, index)\n included = included + usize.ONE\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n start = usize.ZERO\n index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDot(relative, start, index) == false {\n if isDotDot(relative, start, index) == false {\n if survivingRelative(relative, index + usize.ONE) {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeRelativeComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeRelativeComponent, relative, start, index)\n included = included + usize.ONE\n }\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return run finishPath(move combined)\n}\n\n/// Allocates an independently owned parent path, or [`None`] when `self` is root.\n///\n/// # Details\n///\n/// The result does not borrow `self`. A direct child of root has root as its parent.\npub effect fn parent(\n self: &Path\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n if isRoot(self) { return none() }\n let values = pathBytes(self)\n let nameStart = finalNameStart(values)\n let mut end = usize.ONE\n if nameStart != usize.ONE { end = nameStart - usize.ONE }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, end)\n let owned = run finishPath(move copied)\n return some(move owned)\n}\n\n/// Portable mutable service for normalized paths and whole-file operations.\n///\n/// # Details\n///\n/// Application code supplies one provider lexically with `Effect.provideMut`; tests can implement\n/// this service in memory, while native applications can use `silk.os_filesystem`. The service owns\n/// platform policy, but every implementation must preserve the portable error categories,\n/// create-or-truncate writes, and deterministic listing order described here.\n///\n/// # Examples\n/// ## Write a file after creating its parents\n/// ```silk\n/// import silk.allocator { Allocator }\n///\n/// import silk.filesystem as FileSystem\n///\n/// import silk.usize as usize\n///\n/// pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize\n/// ! FileSystem.FileError | Allocator.OutOfMemoryError\n/// ? &mut FileSystem.FileSystem | &mut Allocator {\n/// let written = run FileSystem.writeFileWithParents(path, contents)\n/// return contents.length\n/// }\n/// ```\npub service FileSystem {\n /// Reads one complete regular file into independently owned bytes.\n ///\n /// # Details\n ///\n /// Reading a directory fails with `WrongType`. Allocation of the returned [`Bytes`] may fail\n /// independently of the provider read.\n effect fn readFile(\n path: &Path\n ) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Writes one complete byte view with create-or-truncate semantics.\n ///\n /// # Details\n ///\n /// A missing file is created; an existing regular file is replaced by exactly `bytes`. The call\n /// does not create missing parent directories—use [`writeFileWithParents`] for that workflow.\n effect fn writeFile(path: &Path, bytes: &[u8]) -> () ! FileError ? &mut FileSystem\n /// Returns [`FileInfo`] or [`DirectoryInfo`] for the path without opening file contents.\n ///\n /// # Details\n ///\n /// Missing paths fail with `NotFound`; providers use `WrongType` only when an operation requires a\n /// particular kind, not for this discriminating query.\n effect fn stat(path: &Path) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem\n /// Returns immediate owned children in deterministic complete-path byte order.\n ///\n /// # Details\n ///\n /// The result is not recursive. Each `DirectoryEntry.path` is independently owned and may be\n /// retained after the listing vector is released.\n effect fn listDirectory(\n path: &Path\n ) -> Vector ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Creates exactly one missing directory whose parent already exists.\n ///\n /// # Details\n ///\n /// Existing paths fail with `AlreadyExists`; use [`createDirectoriesRecursively`] to ensure every\n /// missing component.\n effect fn createDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one regular file and fails with `WrongType` for a directory.\n effect fn removeFile(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one empty directory.\n ///\n /// # Details\n ///\n /// A nonempty directory fails with `NotEmpty`; use [`removeDirectoryRecursively`] only when all\n /// descendants are intentionally in scope for removal.\n effect fn removeDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Creates one directory under an existing parent under a name no other caller holds.\n ///\n /// # Details\n ///\n /// The provider chooses the name\'s unique part and returns the complete Path, because only the\n /// provider can create and claim a name in one step. A caller that supplied the name would have\n /// to check-then-create, and the gap between those two is exactly the race this avoids.\n /// `prefix` is a byte prefix for the provider-chosen child name, not a complete path. The returned\n /// directory already exists and is an immediate child of `parent`.\n effect fn createTemporaryDirectory(\n parent: &Path,\n prefix: &[u8]\n ) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n}\n\n/// A directory a caller owns outright, together with everything written inside it.\n///\n/// # Details\n///\n/// Ownership is affine: `TemporaryDirectory` holds an owned `Path`, so exactly one binding holds\n/// it and the compiler rejects a second use of a moved one. Ownership is not, however, a `Drop`\n/// hook. Removing a directory is a fallible operation that requires the `FileSystem` capability,\n/// and a `Drop` hook may carry neither a failure row nor a requirement row, so a hook here could\n/// only be written by inventing an infallible intrinsic over a fallible syscall. Release is\n/// therefore explicit and honest about both rows — see `release`.\n///\n/// Scope ownership comes from composition rather than from a hook: `Effect.ensuring(release)`\n/// runs the release whatever the protected Effect\'s outcome. Because `ensuring` types its\n/// finalizer `! never`, that composition has to say what a failed removal means; `releaseIgnored`\n/// is the stdlib\'s answer and names the loss at the call site.\npub struct TemporaryDirectory {\n /// The complete owned path callers use while the scope remains live.\n pub path: Path\n}\n\n/// Creates an explicitly owned temporary directory under `parent` with a name beginning in `prefix`.\n///\n/// # Details\n///\n/// The result is owned. Nothing removes it until a caller runs [`release`] or [`releaseIgnored`].\n/// The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in\n/// one operation.\npub effect fn temporaryDirectory(\n parent: &Path,\n prefix: string\n) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let created = run FileSystem.createTemporaryDirectory(parent, stringUtf8Bytes(prefix))\n return TemporaryDirectory { path: move created }\n}\n\n/// Consumes one TemporaryDirectory and removes it together with everything inside it.\n///\n/// # Details\n///\n/// Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the\n/// tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a\n/// failed cleanup uses this operation and handles the failure. The owner is consumed even when\n/// removal fails, so copy any diagnostic path information needed before calling.\npub effect fn release(\n self: TemporaryDirectory\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let owned = move self\n let removed = run removeDirectoryRecursively(&owned.path)\n drop owned\n return ()\n}\n\neffect fn discardReleaseFailure(error: FileError | OutOfMemoryError) -> () { return () }\n\n/// Consumes one TemporaryDirectory, removes it, and discards a failed removal.\n///\n/// # Details\n///\n/// This exists because `Effect.ensuring` types its finalizer `! never`, so a fallible release has\n/// to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a\n/// caller reading `releaseIgnored` at the call site can see that a failed removal is being\n/// dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —\n/// a directory the host will reap — and what is kept is the protected Effect\'s own outcome, which\n/// is the answer the program was computing.\n///\n/// A caller who needs the failure uses `release` instead and does not compose it with `ensuring`.\n///\n/// The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer\n/// starts. Derive the required paths before you give the owner to the finalizer.\npub effect fn releaseIgnored(\n self: TemporaryDirectory\n) -> () ? &mut FileSystem | &mut Allocator {\n return run Effect.catchAll(release(move self), discardReleaseFailure)\n}\n\n/// Copies one recorded Path out of the walk\'s own record.\n///\n/// The walk appends to the same record it is reading, so it reads through a copy rather than\n/// through a borrow that the next append would invalidate.\neffect fn recordedCopy(\n recorded: &Vector,\n index: usize\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return match &vectorAsSlice(recorded)[index] {\n Path { bytes, nameBytes } => run fromBytes(bytesAsSlice(&bytes))\n }\n}\n\n/// Removes a directory, every descendant file, and every descendant directory.\n///\n/// # Details\n///\n/// Two passes, because the portable primitive removes exactly one *empty* directory. The first\n/// pass walks the tree front to back, unlinking every file it meets and recording every directory\n/// it meets; the second removes the recorded directories back to front. That order is\n/// child-before-parent for free: a directory is always recorded before the children found inside\n/// it, so reversing the record reverses the containment. Neither pass recurses, so depth costs\n/// vector capacity rather than stack.\n///\n/// This operation is destructive and not transactional. If a provider or allocation failure occurs,\n/// removals already completed remain completed and the remaining tree is left in place.\npub effect fn removeDirectoryRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let mut recorded = vectorMake()\n let seed = run fromBytes(rawBytes(path))\n let noted = run vectorAppend(&mut recorded, move seed)\n let mut index = usize.ZERO\n while index < vectorLength(&recorded) {\n let current = run recordedCopy(&recorded, index)\n let entries = run FileSystem.listDirectory(¤t)\n let listed = vectorAsSlice(&entries)\n let mut cursor = usize.ZERO\n while cursor < listed.length {\n let childKind = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } => borrowedKindCode(&childEntryKind)\n }\n if childKind == 0 {\n let unlinked = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run FileSystem.removeFile(&childPath)\n }\n } else {\n let toRemove = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run fromBytes(rawBytes(&childPath))\n }\n let notedChild = run vectorAppend(&mut recorded, move toRemove)\n }\n cursor = cursor + usize.ONE\n }\n index = index + usize.ONE\n }\n while usize.ZERO < vectorLength(&recorded) {\n let taken = vectorPop(&mut recorded)\n let emptied = match move taken {\n Option.Some { value: selected } => move selected\n Option.None => run fromBytes(rawBytes(path))\n }\n let removed = run FileSystem.removeDirectory(&emptied)\n }\n return ()\n}\n\nstruct DirectoryPresent {}\nstruct DirectoryMissing {}\nstruct DirectoryWrongType {}\nstruct DirectoryStatFailure { error: FileError }\n\nfn classifyStatFailure(\n failure: FileError\n) -> DirectoryMissing | DirectoryStatFailure {\n if failure.reason.code == 0 { return DirectoryMissing {} }\n return DirectoryStatFailure { error: move failure }\n}\n\nfn classifyDirectory(\n outcome: Result\n) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure {\n return match move outcome {\n Result.Success { value: info } => match move info {\n DirectoryInfo {} => DirectoryPresent {}\n FileInfo { byteLength } => DirectoryWrongType {}\n }\n Result.Failure { error: failure } => classifyStatFailure(move failure)\n }\n}\n\n/// Ensures that `path` and every missing ancestor exist as directories.\n///\n/// # Details\n///\n/// Existing directories are kept. An existing regular file at any component fails with\n/// `WrongType`; failures other than `NotFound` propagate. This is ordinary stat-then-create\n/// composition, so concurrent namespace changes may still race according to provider policy.\npub effect fn createDirectoriesRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let values = pathBytes(path)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return ()\n}\n\n/// Ensures every parent directory exists, then writes the complete byte view to `path`.\n///\n/// # Details\n///\n/// The final write uses `FileSystem.writeFile` create-or-truncate semantics. Passing root delegates\n/// directly to the provider and normally fails with `WrongType`. Directory creation and writing are\n/// not transactional, so a later failure may leave newly created parents behind.\npub effect fn writeFileWithParents(\n path: &Path,\n bytes: &[u8]\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n if isRoot(path) { return run FileSystem.writeFile(path, bytes) }\n let pathValues = pathBytes(path)\n let nameStart = finalNameStart(pathValues)\n let mut parentEnd = usize.ONE\n if nameStart != usize.ONE { parentEnd = nameStart - usize.ONE }\n let parentBytes = run appendRange(bytesMake(), pathValues, usize.ZERO, parentEnd)\n let ownedParent = run finishPath(move parentBytes)\n let values = pathBytes(&ownedParent)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return run FileSystem.writeFile(path, bytes)\n}\n\neffect fn existsFailure(failure: FileError) -> bool ! FileError {\n if failure.reason.code == 0 { return false }\n return run raise(move failure)\n}\n\n/// Returns whether a file or directory exists at `path`.\n///\n/// # Details\n///\n/// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider\n/// failure propagate so callers cannot mistake an inaccessible path for an absent one.\npub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem {\n let completed = run Intrinsic.effectResult(FileSystem.stat(path))\n return match move completed {\n Result.Success { value: info } => true\n Result.Failure { error: failure } => run existsFailure(move failure)\n }\n}\n', }, { module: 'silk/format', path: 'silk/format.silk', sourceIdentity: 'silk/format', - digest: 'f059f15976a0cb0d82044c2f419fe623dee17a3d6b2d74054bd9a8788625503b', + digest: '5f0223d568e8146f2a0b529be3ccfc5d70d6ed1f666518c3b7786d7337ce8c7e', documentation: 'silk/format.silk', layer: 'portable', runtimeInventory: [], namespace: 'Format', aliases: ['NotANumber', 'OutOfRange', 'ParseError'], source: - '//! Decimal rendering and complete-text parsing shared by every integer module.\n//!\n//! # When to use\n//! Prefer an integer module\'s `toText` and `parse` operations when the width is already known. Use\n//! [`unsignedText`], [`signedText`], [`unsignedValue`], or [`signedValue`] when intentionally\n//! working through the widest integer representation.\n//!\n//! # Details\n//! Rendering uses radix 10 and allocates an owned [`String`]. Parsing is allocation-free and reads\n//! the entire input: whitespace, a leading `+`, or trailing characters produce [`NotANumber`],\n//! while syntactically valid values outside the destination range produce [`OutOfRange`]. Signed\n//! parsing accepts `-0`; unsigned parsing rejects any sign.\n//!\n//! # Examples\n//! ## Parse and render one signed integer\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.format as Format\n//!\n//! import silk.i64 as i64\n//!\n//! import silk.result as Result\n//!\n//! import silk.string as String\n//!\n//! effect fn convert() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let parsed = Format.signedValue("-42")\n//! |> Result.unwrapOr(0)\n//! let rendering = Format.signedText(parsed)\n//! |> Effect.provideMut(&mut allocator)\n//! let rendered = run rendering\n//! let text = String.view(&rendered)\n//! if text == "-42" {} else {\n//! return 0\n//! }\n//! return 0 - parsed\n//! |> i64.toI32\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(convert(), recover)\n//! }\n//! ```\n\n// Decimal text for integer values, in both directions, at radix 10 only.\n//\n// The whole engine is written twice — once over `u64` and once over `i64` — and every fixed-width\n// integer module reaches it by widening to one of those two. Writing it once and generically is not\n// available: an interface may only carry operations an operator spells, so `checkedMultiply` and\n// friends have no call surface through a bound. Widening is lossless in both directions, so the two\n// implementations are the whole truth about every integer type.\n//\n// Rendering walks digits most-significant first, which is why it computes the leading power of ten\n// before it emits anything: `String.append` only appends, and a least-significant-first walk would\n// need to prepend. Parsing accumulates *negatively* for signed text, so `i64.MIN` — whose magnitude\n// has no positive counterpart — parses like any other value instead of being a special case.\n\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, Some, None }\nimport silk.result { Result, Success, Failure, failResult, succeed }\nimport silk.string {\n String,\n make as stringMake,\n append as stringAppend,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\nconst ZERO: u64 = 0\nconst ONE: u64 = 1\nconst TEN: u64 = 10\n\nconst SIGNED_ZERO: i64 = 0\nconst SIGNED_ONE: i64 = 1\nconst SIGNED_TEN: i64 = 10\nconst NEGATIVE_TEN: i64 = -10\n\nconst ASCII_MINUS: u8 = 45\nconst ASCII_ZERO: u8 = 48\nconst ASCII_NINE: u8 = 57\n\n/// The byte offset where complete decimal parsing cannot continue.\npub struct NotANumber {\n /// The offset where reading stopped. It can equal the byte length when a digit was required.\n pub offset: usize\n}\n\n/// A well-formed decimal number whose value does not fit the requested type.\npub struct OutOfRange {}\n\n/// Why decimal text did not produce a value, narrowed with `match`.\npub struct ParseError {\n /// The reason the text was rejected.\n pub reason: NotANumber | OutOfRange\n}\n\nfn reject(reason: NotANumber | OutOfRange) -> Result {\n return failResult(ParseError { reason: move reason })\n}\n\nfn notANumber(offset: usize) -> Result {\n return reject(NotANumber { offset: offset })\n}\n\nfn outOfRange() -> Result {\n return reject(OutOfRange {})\n}\n\n// One decimal digit as its own text. Ten literals rather than arithmetic on bytes, because the\n// result must be `string` and no owned byte becomes text without allocating.\nfn digitText(digit: i32) -> string {\n if digit == 1 { return "1" }\n if digit == 2 { return "2" }\n if digit == 3 { return "3" }\n if digit == 4 { return "4" }\n if digit == 5 { return "5" }\n if digit == 6 { return "6" }\n if digit == 7 { return "7" }\n if digit == 8 { return "8" }\n if digit == 9 { return "9" }\n return "0"\n}\n\n// The value of one ASCII decimal digit, or -1 for any other byte.\nfn digitValue(value: u8) -> i32 {\n if value < ASCII_ZERO { return -1 }\n if ASCII_NINE < value { return -1 }\n return u8.toI32(value) - u8.toI32(ASCII_ZERO)\n}\n\n/// Renders an unsigned value as decimal text in freshly owned storage.\n///\n/// # Details\n///\n/// The result contains ASCII decimal digits without a sign or leading zeroes. Zero produces `"0"`.\npub effect fn unsignedText(value: u64) -> String ! OutOfMemoryError ? &mut Allocator {\n let mut text = stringMake()\n let mut divisor = ONE\n while TEN <= value / divisor { divisor = divisor * TEN }\n while ZERO < divisor {\n let digit = (value / divisor) % TEN\n let appended = run stringAppend(&mut text, digitText(u64.toI32(digit)))\n divisor = divisor / TEN\n }\n return move text\n}\n\n/// Renders a signed value as decimal text in freshly owned storage, with a leading `-` when negative.\n///\n/// # Details\n///\n/// The result contains ASCII decimal digits without leading zeroes. Zero produces `"0"`.\n/// The complete `i64` range, including `i64.MIN`, is supported.\npub effect fn signedText(value: i64) -> String ! OutOfMemoryError ? &mut Allocator {\n let mut text = stringMake()\n if value < SIGNED_ZERO {\n let sign = run stringAppend(&mut text, "-")\n }\n let mut negated = value\n if SIGNED_ZERO < negated { negated = SIGNED_ZERO - negated }\n let mut divisor = SIGNED_ONE\n while negated / divisor <= NEGATIVE_TEN { divisor = divisor * SIGNED_TEN }\n while SIGNED_ZERO < divisor {\n let digit = SIGNED_ZERO - ((negated / divisor) % SIGNED_TEN)\n let appended = run stringAppend(&mut text, digitText(i64.toI32(digit)))\n divisor = divisor / SIGNED_TEN\n }\n return move text\n}\n\n/// Reads complete decimal text as an unsigned value.\n///\n/// # Details\n///\n/// Empty text, a leading sign, and any byte outside `0`–`9` are `NotANumber` at the offset that\n/// stopped the read. A value above `u64.MAX` is `OutOfRange`, detected before the overflow rather\n/// than after it.\npub fn unsignedValue(text: string) -> Result {\n let bytes = stringUtf8Bytes(text)\n if bytes.length == usize.ZERO { return notANumber(usize.ZERO) }\n let mut index = usize.ZERO\n let mut total = ZERO\n while index < bytes.length {\n let digit = digitValue(bytes[index])\n if digit < 0 { return notANumber(index) }\n if u64.MAX / TEN < total { return outOfRange() }\n total = total * TEN\n let addend = i32.toU64(digit)\n if u64.MAX - addend < total { return outOfRange() }\n total = total + addend\n index = index + usize.ONE\n }\n return succeed(total)\n}\n\n/// Reads complete decimal text as a signed value, accepting one leading `-`.\n///\n/// # Details\n///\n/// Digits accumulate negatively, so text naming `i64.MIN` reads like any other value. A leading `+`\n/// is not accepted. A value outside `i64.MIN`–`i64.MAX` is `OutOfRange`.\npub fn signedValue(text: string) -> Result {\n let bytes = stringUtf8Bytes(text)\n let mut index = usize.ZERO\n let mut negative = false\n if usize.ZERO < bytes.length {\n if bytes[usize.ZERO] == ASCII_MINUS {\n negative = true\n index = usize.ONE\n }\n }\n if index == bytes.length { return notANumber(index) }\n let mut total = SIGNED_ZERO\n while index < bytes.length {\n let digit = digitValue(bytes[index])\n if digit < 0 { return notANumber(index) }\n if total < i64.MIN / SIGNED_TEN { return outOfRange() }\n total = total * SIGNED_TEN\n let subtrahend = i32.toI64(digit)\n if total < i64.MIN + subtrahend { return outOfRange() }\n total = total - subtrahend\n index = index + usize.ONE\n }\n if negative { return succeed(total) }\n if total < SIGNED_ZERO - i64.MAX { return outOfRange() }\n return succeed(SIGNED_ZERO - total)\n}\n\n// Narrowing is the checked conversion rather than a comparison against the target\'s MAX, so every\n// type reads the same way. It is also the only form that works at every width: `usize.MAX` and\n// `isize.MAX` are target-dependent, so a comparison against them would be a different comparison\n// per target, while the checked conversion is one range test whatever the pointer width.\n\nfn narrowU8(value: u64) -> Result {\n return match move u64.checkedToU8(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowU16(value: u64) -> Result {\n return match move u64.checkedToU16(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowU32(value: u64) -> Result {\n return match move u64.checkedToU32(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowUsize(value: u64) -> Result {\n return match move u64.checkedToUsize(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowI8(value: i64) -> Result {\n return match move i64.checkedToI8(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowI16(value: i64) -> Result {\n return match move i64.checkedToI16(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowI32(value: i64) -> Result {\n return match move i64.checkedToI32(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowIsize(value: i64) -> Result {\n return match move i64.checkedToIsize(value) {\n None {} => outOfRange()\n Some { value: narrowed } => succeed(narrowed)\n }\n}\n\n/// Reads complete decimal text as a `u8`, rejecting a value above `u8.MAX`.\npub fn u8Value(text: string) -> Result {\n return match move unsignedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowU8(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as a `u16`, rejecting a value above `u16.MAX`.\npub fn u16Value(text: string) -> Result {\n return match move unsignedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowU16(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as a `u32`, rejecting a value above `u32.MAX`.\npub fn u32Value(text: string) -> Result {\n return match move unsignedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowU32(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as a `u64`, rejecting a value above `u64.MAX`.\npub fn u64Value(text: string) -> Result {\n return unsignedValue(text)\n}\n\n/// Reads complete decimal text as a `usize`, rejecting a value the target\'s pointer width cannot\n/// hold.\npub fn usizeValue(text: string) -> Result {\n return match move unsignedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowUsize(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as an `i8`, rejecting a value outside `i8.MIN`–`i8.MAX`.\npub fn i8Value(text: string) -> Result {\n return match move signedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowI8(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as an `i16`, rejecting a value outside `i16.MIN`–`i16.MAX`.\npub fn i16Value(text: string) -> Result {\n return match move signedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowI16(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as an `i32`, rejecting a value outside `i32.MIN`–`i32.MAX`.\npub fn i32Value(text: string) -> Result {\n return match move signedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowI32(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Reads complete decimal text as an `i64`, rejecting a value outside `i64.MIN`–`i64.MAX`.\npub fn i64Value(text: string) -> Result {\n return signedValue(text)\n}\n\n/// Reads complete decimal text as an `isize`, rejecting a value the target\'s pointer width cannot\n/// hold.\npub fn isizeValue(text: string) -> Result {\n return match move signedValue(text) {\n Result { value: outcome } => match move outcome {\n Success { value } => narrowIsize(value)\n Failure { error } => failResult(move error)\n }\n }\n}\n', + '//! Decimal rendering and complete-text parsing shared by every integer module.\n//!\n//! # When to use\n//! Prefer an integer module\'s `toText` and `parse` operations when the width is already known. Use\n//! [`unsignedText`], [`signedText`], [`unsignedValue`], or [`signedValue`] when intentionally\n//! working through the widest integer representation.\n//!\n//! # Details\n//! Rendering uses radix 10 and allocates an owned [`String`]. Parsing is allocation-free and reads\n//! the entire input: whitespace, a leading `+`, or trailing characters produce [`NotANumber`],\n//! while syntactically valid values outside the destination range produce [`OutOfRange`]. Signed\n//! parsing accepts `-0`; unsigned parsing rejects any sign.\n//!\n//! # Examples\n//! ## Parse and render one signed integer\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.format as Format\n//!\n//! import silk.i64 as i64\n//!\n//! import silk.result as Result\n//!\n//! import silk.string as String\n//!\n//! effect fn convert() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let parsed = Format.signedValue("-42")\n//! |> Result.unwrapOr(0)\n//! let rendering = Format.signedText(parsed)\n//! |> Effect.provideMut(&mut allocator)\n//! let rendered = run rendering\n//! let text = String.view(&rendered)\n//! if text == "-42" {} else {\n//! return 0\n//! }\n//! return 0 - parsed\n//! |> i64.toI32\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(convert(), recover)\n//! }\n//! ```\n\n// Decimal text for integer values, in both directions, at radix 10 only.\n//\n// The whole engine is written twice — once over `u64` and once over `i64` — and every fixed-width\n// integer module reaches it by widening to one of those two. Writing it once and generically is not\n// available: an interface may only carry operations an operator spells, so `checkedMultiply` and\n// friends have no call surface through a bound. Widening is lossless in both directions, so the two\n// implementations are the whole truth about every integer type.\n//\n// Rendering walks digits most-significant first, which is why it computes the leading power of ten\n// before it emits anything: `String.append` only appends, and a least-significant-first walk would\n// need to prepend. Parsing accumulates *negatively* for signed text, so `i64.MIN` — whose magnitude\n// has no positive counterpart — parses like any other value instead of being a special case.\n\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result, failResult, succeed }\nimport silk.string {\n String,\n make as stringMake,\n append as stringAppend,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\nconst ZERO: u64 = 0\nconst ONE: u64 = 1\nconst TEN: u64 = 10\n\nconst SIGNED_ZERO: i64 = 0\nconst SIGNED_ONE: i64 = 1\nconst SIGNED_TEN: i64 = 10\nconst NEGATIVE_TEN: i64 = -10\n\nconst ASCII_MINUS: u8 = 45\nconst ASCII_ZERO: u8 = 48\nconst ASCII_NINE: u8 = 57\n\n/// The byte offset where complete decimal parsing cannot continue.\npub struct NotANumber {\n /// The offset where reading stopped. It can equal the byte length when a digit was required.\n pub offset: usize\n}\n\n/// A well-formed decimal number whose value does not fit the requested type.\npub struct OutOfRange {}\n\n/// Why decimal text did not produce a value, narrowed with `match`.\npub struct ParseError {\n /// The reason the text was rejected.\n pub reason: NotANumber | OutOfRange\n}\n\nfn reject(reason: NotANumber | OutOfRange) -> Result {\n return failResult(ParseError { reason: move reason })\n}\n\nfn notANumber(offset: usize) -> Result {\n return reject(NotANumber { offset: offset })\n}\n\nfn outOfRange() -> Result {\n return reject(OutOfRange {})\n}\n\n// One decimal digit as its own text. Ten literals rather than arithmetic on bytes, because the\n// result must be `string` and no owned byte becomes text without allocating.\nfn digitText(digit: i32) -> string {\n if digit == 1 { return "1" }\n if digit == 2 { return "2" }\n if digit == 3 { return "3" }\n if digit == 4 { return "4" }\n if digit == 5 { return "5" }\n if digit == 6 { return "6" }\n if digit == 7 { return "7" }\n if digit == 8 { return "8" }\n if digit == 9 { return "9" }\n return "0"\n}\n\n// The value of one ASCII decimal digit, or -1 for any other byte.\nfn digitValue(value: u8) -> i32 {\n if value < ASCII_ZERO { return -1 }\n if ASCII_NINE < value { return -1 }\n return u8.toI32(value) - u8.toI32(ASCII_ZERO)\n}\n\n/// Renders an unsigned value as decimal text in freshly owned storage.\n///\n/// # Details\n///\n/// The result contains ASCII decimal digits without a sign or leading zeroes. Zero produces `"0"`.\npub effect fn unsignedText(value: u64) -> String ! OutOfMemoryError ? &mut Allocator {\n let mut text = stringMake()\n let mut divisor = ONE\n while TEN <= value / divisor { divisor = divisor * TEN }\n while ZERO < divisor {\n let digit = (value / divisor) % TEN\n let appended = run stringAppend(&mut text, digitText(u64.toI32(digit)))\n divisor = divisor / TEN\n }\n return move text\n}\n\n/// Renders a signed value as decimal text in freshly owned storage, with a leading `-` when negative.\n///\n/// # Details\n///\n/// The result contains ASCII decimal digits without leading zeroes. Zero produces `"0"`.\n/// The complete `i64` range, including `i64.MIN`, is supported.\npub effect fn signedText(value: i64) -> String ! OutOfMemoryError ? &mut Allocator {\n let mut text = stringMake()\n if value < SIGNED_ZERO {\n let sign = run stringAppend(&mut text, "-")\n }\n let mut negated = value\n if SIGNED_ZERO < negated { negated = SIGNED_ZERO - negated }\n let mut divisor = SIGNED_ONE\n while negated / divisor <= NEGATIVE_TEN { divisor = divisor * SIGNED_TEN }\n while SIGNED_ZERO < divisor {\n let digit = SIGNED_ZERO - ((negated / divisor) % SIGNED_TEN)\n let appended = run stringAppend(&mut text, digitText(i64.toI32(digit)))\n divisor = divisor / SIGNED_TEN\n }\n return move text\n}\n\n/// Reads complete decimal text as an unsigned value.\n///\n/// # Details\n///\n/// Empty text, a leading sign, and any byte outside `0`–`9` are `NotANumber` at the offset that\n/// stopped the read. A value above `u64.MAX` is `OutOfRange`, detected before the overflow rather\n/// than after it.\npub fn unsignedValue(text: string) -> Result {\n let bytes = stringUtf8Bytes(text)\n if bytes.length == usize.ZERO { return notANumber(usize.ZERO) }\n let mut index = usize.ZERO\n let mut total = ZERO\n while index < bytes.length {\n let digit = digitValue(bytes[index])\n if digit < 0 { return notANumber(index) }\n if u64.MAX / TEN < total { return outOfRange() }\n total = total * TEN\n let addend = i32.toU64(digit)\n if u64.MAX - addend < total { return outOfRange() }\n total = total + addend\n index = index + usize.ONE\n }\n return succeed(total)\n}\n\n/// Reads complete decimal text as a signed value, accepting one leading `-`.\n///\n/// # Details\n///\n/// Digits accumulate negatively, so text naming `i64.MIN` reads like any other value. A leading `+`\n/// is not accepted. A value outside `i64.MIN`–`i64.MAX` is `OutOfRange`.\npub fn signedValue(text: string) -> Result {\n let bytes = stringUtf8Bytes(text)\n let mut index = usize.ZERO\n let mut negative = false\n if usize.ZERO < bytes.length {\n if bytes[usize.ZERO] == ASCII_MINUS {\n negative = true\n index = usize.ONE\n }\n }\n if index == bytes.length { return notANumber(index) }\n let mut total = SIGNED_ZERO\n while index < bytes.length {\n let digit = digitValue(bytes[index])\n if digit < 0 { return notANumber(index) }\n if total < i64.MIN / SIGNED_TEN { return outOfRange() }\n total = total * SIGNED_TEN\n let subtrahend = i32.toI64(digit)\n if total < i64.MIN + subtrahend { return outOfRange() }\n total = total - subtrahend\n index = index + usize.ONE\n }\n if negative { return succeed(total) }\n if total < SIGNED_ZERO - i64.MAX { return outOfRange() }\n return succeed(SIGNED_ZERO - total)\n}\n\n// Narrowing is the checked conversion rather than a comparison against the target\'s MAX, so every\n// type reads the same way. It is also the only form that works at every width: `usize.MAX` and\n// `isize.MAX` are target-dependent, so a comparison against them would be a different comparison\n// per target, while the checked conversion is one range test whatever the pointer width.\n\nfn narrowU8(value: u64) -> Result {\n return match move u64.checkedToU8(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowU16(value: u64) -> Result {\n return match move u64.checkedToU16(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowU32(value: u64) -> Result {\n return match move u64.checkedToU32(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowUsize(value: u64) -> Result {\n return match move u64.checkedToUsize(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowI8(value: i64) -> Result {\n return match move i64.checkedToI8(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowI16(value: i64) -> Result {\n return match move i64.checkedToI16(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowI32(value: i64) -> Result {\n return match move i64.checkedToI32(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\nfn narrowIsize(value: i64) -> Result {\n return match move i64.checkedToIsize(value) {\n Option.None => outOfRange()\n Option.Some { value: narrowed } => succeed(narrowed)\n }\n}\n\n/// Reads complete decimal text as a `u8`, rejecting a value above `u8.MAX`.\npub fn u8Value(text: string) -> Result {\n return match move unsignedValue(text) {\n Result.Success { value } => narrowU8(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as a `u16`, rejecting a value above `u16.MAX`.\npub fn u16Value(text: string) -> Result {\n return match move unsignedValue(text) {\n Result.Success { value } => narrowU16(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as a `u32`, rejecting a value above `u32.MAX`.\npub fn u32Value(text: string) -> Result {\n return match move unsignedValue(text) {\n Result.Success { value } => narrowU32(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as a `u64`, rejecting a value above `u64.MAX`.\npub fn u64Value(text: string) -> Result {\n return unsignedValue(text)\n}\n\n/// Reads complete decimal text as a `usize`, rejecting a value the target\'s pointer width cannot\n/// hold.\npub fn usizeValue(text: string) -> Result {\n return match move unsignedValue(text) {\n Result.Success { value } => narrowUsize(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as an `i8`, rejecting a value outside `i8.MIN`–`i8.MAX`.\npub fn i8Value(text: string) -> Result {\n return match move signedValue(text) {\n Result.Success { value } => narrowI8(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as an `i16`, rejecting a value outside `i16.MIN`–`i16.MAX`.\npub fn i16Value(text: string) -> Result {\n return match move signedValue(text) {\n Result.Success { value } => narrowI16(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as an `i32`, rejecting a value outside `i32.MIN`–`i32.MAX`.\npub fn i32Value(text: string) -> Result {\n return match move signedValue(text) {\n Result.Success { value } => narrowI32(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Reads complete decimal text as an `i64`, rejecting a value outside `i64.MIN`–`i64.MAX`.\npub fn i64Value(text: string) -> Result {\n return signedValue(text)\n}\n\n/// Reads complete decimal text as an `isize`, rejecting a value the target\'s pointer width cannot\n/// hold.\npub fn isizeValue(text: string) -> Result {\n return match move signedValue(text) {\n Result.Success { value } => narrowIsize(value)\n Result.Failure { error } => failResult(move error)\n }\n}\n', }, { module: 'silk/hash', @@ -304,44 +304,44 @@ export const modules = [ module: 'silk/hash_map', path: 'silk/hash_map.silk', sourceIdentity: 'silk/hash_map', - digest: '7d6de19840adddc5e08605dd4359f35078ea6961d8e1ff8e4a658859aa622845', + digest: '56df84ad61e1cabacc670d5a8368d1425e57d08debda05a1060a8855a86b3b49', documentation: 'silk/hash_map.silk', layer: 'portable', runtimeInventory: ['NonParking', 'replace'], namespace: 'HashMap', source: - "//! Owned key-value storage with deterministic seeded hashing and open-addressed lookup.\n//!\n//! # When to use\n//! Use [`HashMap`] for key-based lookup when a key has a [`HashKey`] witness. Use\n//! `silk.vector.Vector` when presentation order should follow insertion, or when indexing rather\n//! than equivalence is the primary operation.\n//!\n//! # Details\n//! The map starts allocation-free, first allocates eight buckets, and grows before used buckets\n//! exceed three quarters of the table. Linear probing crosses removal markers; growth doubles the\n//! table, moves every live entry, and discards those markers. Allocation completes before the map\n//! commits, so failed growth leaves existing entries, length, and bucket count unchanged.\n//!\n//! One [`HashSeed`] plus the same operation sequence fixes bucket presentation order across the\n//! evaluator, native code, and WebAssembly. Iterate deterministically by scanning\n//! `0..bucketCount`, testing [`occupiedAt`], then reading [`keyAt`] and [`valueAt`]. This is bucket\n//! order, not insertion order.\n//!\n//! # Gotchas\n//! Lookup and removal consume the probe key. [`get`], [`keyAt`], and [`valueAt`] copy a complete\n//! stored entry and therefore require both stored key and value to be `Copy`; use [`withMut`] to\n//! update a move-only value in place or [`remove`] to transfer it out. Equivalent keys must also\n//! obey the [`HashKey`] hash contract.\n//!\n//! # Examples\n//! ## Insert, read, and remove one value\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.hash as Hash\n//!\n//! import silk.hash_map as HashMap\n//!\n//! import silk.option as Option\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut map = HashMap.make(Hash.seed(17))\n//! let inserting = HashMap.insert(&mut map, Hash.word(7), 42)\n//! |> Effect.provideMut(&mut allocator)\n//! let previous = run inserting\n//! drop previous\n//! let found = HashMap.get(&map, Hash.word(7))\n//! |> Option.unwrapOr(0)\n//! let removed = HashMap.remove(&mut map, Hash.word(7))\n//! drop removed\n//! if HashMap.contains(&map, Hash.word(7)) {\n//! return 0\n//! }\n//! return found\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\n// A hashed map over the allocation substrate. Ordinary Silk: no compiler phase knows this type, no\n// engine contains a hash operation, and every hash a program computes is a call to a function some\n// `HashKey` witness declared in Silk.\n//\n// Open addressing with linear probing over two buffers of one width: the entries, whose slot is\n// initialized exactly when its state byte says occupied, and the states — vacant, occupied, or\n// removed. A removed slot keeps a probe run intact after a removal, and growth drops the removed\n// marks because it rehomes only the occupied entries.\n//\n// Each entry carries the hash it was placed under. That is what lets growth rehome an entry without\n// hashing it again, which matters here beyond the saved work: a bound is not carried into a nested\n// generic call, so a `K: HashKey` body cannot hand its own `K` to another `K: HashKey` function.\n// Anything that needs the witness has to be written in the body that needs it. Storing the hash\n// keeps growth, placement, and release ordinary unbounded code, and leaves the witness reached from\n// exactly the five operations that take a key from the caller.\n//\n// Every bucket index is reduced from the hash in u64 arithmetic before it narrows to `usize`, so the\n// index a key lands on does not depend on the width of a pointer and the iteration order is the same\n// under the evaluator, under the native backend, and under WebAssembly.\n\nimport silk.bool as bool\nimport silk.allocator as AllocationFailure\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.hash { HashKey, HashSeed }\nimport silk.layout { Layout }\nimport silk.layout { LayoutOverflow }\nimport silk.option { Option, Some, None }\nimport silk.raw_buffer as RawBuffer\nimport silk.slot as Slot\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A slot is vacant, and no key was ever placed here.\nconst VACANT: u8 = 0\n\n/// A slot holds an initialized entry.\nconst OCCUPIED: u8 = 1\n\n/// A slot held an entry that was removed. Probing continues through it; lookup does not stop.\nconst REMOVED: u8 = 2\n\n/// The width a map takes when it first allocates.\nconst INITIAL_WIDTH: usize = 8\n\n/// Internal key-value record exposed by the current table representation.\npub struct Entry {\n hash: u64\n key: K\n value: V\n}\n\nimpl Copy for Entry {}\n\n/// Allocation-free storage state used before the map creates its first table.\npub struct Unallocated {\n anchor: [Entry; 0]\n}\n\n/// Allocated entry and occupancy buffers used by [`HashMap`].\npub struct Table {\n entries: RawBuffer>\n states: RawBuffer\n}\n\n/// Owns unique keys and their values under one equivalence, hash witness, and seed.\n///\n/// # Details\n///\n/// An equivalent insertion replaces the stored value instead of adding another entry. The seed\n/// and operation sequence determine bucket presentation order, which is not insertion order.\npub struct HashMap {\n storage: Unallocated | Table\n seed: HashSeed\n length: usize\n used: usize\n capacity: usize\n}\n\n// Diverges on the arms the surrounding logic has already proven impossible.\nfn absurd() -> T {\n let boom = 1 / 0\n return absurd()\n}\n\n/// Constructs an empty map whose every hash is computed under one seed.\n///\n/// # Details\n///\n/// An empty map allocates nothing. The seed fixes the order the map will present its entries in,\n/// and is the only thing besides the sequence of operations that decides it.\npub fn make(seed: HashSeed) -> HashMap {\n return HashMap {\n storage: Unallocated { anchor: [] },\n seed: move seed,\n length: usize.ZERO,\n used: usize.ZERO,\n capacity: usize.ZERO,\n }\n}\n\n/// Returns the number of entries the map holds.\npub fn length(self: &HashMap) -> usize {\n return self.length\n}\n\n/// Returns the number of buckets the map presents, which is the range `occupiedAt` accepts.\npub fn bucketCount(self: &HashMap) -> usize {\n return self.capacity\n}\n\n/// Reports whether one bucket holds an entry. Out-of-range buckets hold nothing.\npub fn occupiedAt(self: &HashMap, index: usize) -> bool {\n if self.capacity <= index {\n return false\n }\n return stateAt(self, index) == OCCUPIED\n}\n\nfn stateAt(self: &HashMap, index: usize) -> u8 {\n return match &self.storage {\n Unallocated nothing => VACANT\n Table { entries, states } => firstByte(RawBuffer.view(&states, index, usize.ONE))\n }\n}\n\nfn firstByte(seen: &[u8]) -> u8 {\n return seen[usize.ZERO]\n}\n\nfn entryAt(self: &HashMap, index: usize) -> &[Entry] {\n return match &self.storage {\n Unallocated { anchor } => emptySlice(&anchor)\n Table { entries, states } => RawBuffer.view>(&entries, index, usize.ONE)\n }\n}\n\nfn emptySlice(anchor: &[Entry]) -> &[Entry] {\n return anchor\n}\n\n/// Reduces a hash to a bucket index in u64 arithmetic, so the index does not depend on the width of\n/// `usize` and one seed therefore fixes one order in every engine.\nfn bucketOf(hashed: u64, width: usize) -> usize {\n return u64.toUsize(u64.remainder(hashed, usize.toU64(width)))\n}\n\nfn advance(index: usize, width: usize) -> usize {\n let next = index + usize.ONE\n if next == width {\n return usize.ZERO\n }\n return next\n}\n\nimpl Drop for HashMap {\n fn drop(self: &mut HashMap) -> () {\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let width = Intrinsic.replace(self.capacity, usize.ZERO)\n let emptied = Intrinsic.replace(self.length, usize.ZERO)\n return match move storage {\n Unallocated nothing => ()\n Table full => releaseFull(move full, width)\n }\n }\n}\n\n// Releases every occupied entry exactly once and then both buffers, so a map dropped while\n// non-empty destroys the keys and values it still owns rather than leaking them.\nfn releaseFull(full: Table, width: usize) -> () {\n let mut owned = move full\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let cleared = Slot.dropValue(move selected)\n }\n index = index + usize.ONE\n }\n drop owned\n }\n return ()\n}\n\nfn openStorage(storage: Unallocated | Table) -> Table {\n return match move storage {\n Unallocated nothing => absurd>()\n Table full => move full\n }\n}\n\neffect fn overflowed() -> Layout ! OutOfMemoryError {\n return run AllocationFailure.outOfMemory()\n}\n\n// Both buffers exist before either is installed, so an allocation that fails releases the one\n// already taken and leaves the caller's map exactly as it was.\neffect fn allocateTable(count: usize) -> Table ! OutOfMemoryError ? &mut Allocator {\n let states = run vacantStates(count)\n let entries = run entryStorage(count)\n return Table { entries: move entries, states: move states }\n}\n\neffect fn vacantStates(count: usize) -> RawBuffer ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return markVacant(move allocation, count)\n}\n\nfn markVacant(allocation: Allocation, count: usize) -> RawBuffer {\n unsafe {\n let mut made = RawBuffer.from(move allocation, count)\n let mut index = usize.ZERO\n while index < count {\n let selected = RawBuffer.slot(&mut made, index)\n let written = Slot.write(move selected, VACANT)\n index = index + usize.ONE\n }\n return move made\n }\n return absurd>()\n}\n\neffect fn entryStorage(count: usize) -> RawBuffer> ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of>()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return adopt(move allocation, count)\n}\n\nfn adopt(allocation: Allocation, count: usize) -> RawBuffer> {\n unsafe {\n return RawBuffer.from>(move allocation, count)\n }\n return absurd>>()\n}\n\n/// Reports whether one more entry would take the map past three quarters of its buckets.\nfn needsRoom(used: usize, width: usize) -> bool {\n if width == usize.ZERO {\n return true\n }\n let quarter = width / (usize.ONE + usize.ONE + usize.ONE + usize.ONE)\n return width - quarter < used + usize.ONE\n}\n\nfn grownWidth(width: usize) -> usize {\n if width == usize.ZERO {\n return INITIAL_WIDTH\n }\n return width + width\n}\n\n// Rehomes every occupied entry into the replacement table under the hash it was placed with, then\n// releases the old buffers. The removed marks do not travel, so growth reclaims them.\nfn migrate(\n storage: Unallocated | Table,\n width: usize,\n fresh: Table,\n next: usize\n) -> Table {\n return match move storage {\n Unallocated nothing => move fresh\n Table full => rehome(move full, width, move fresh, next)\n }\n}\n\nfn rehome(old: Table, width: usize, fresh: Table, next: usize) -> Table {\n let mut source = move old\n let mut target = move fresh\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&source.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut source.entries, index)\n let carried = Slot.take(move selected)\n let placed = settle(&mut target, move carried, next)\n }\n index = index + usize.ONE\n }\n drop source\n }\n return move target\n}\n\n// Places one entry at the first slot its own probe run does not already occupy. Every slot of a\n// replacement table is vacant, so the run always ends.\nfn settle(target: &mut Table, carried: Entry, next: usize) -> () {\n let hashed = entryHash(&carried)\n let mut index = bucketOf(hashed, next)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&target.states, usize.ZERO, next)\n if seen[index] == OCCUPIED {\n index = advance(index, next)\n } else {\n searching = false\n }\n }\n let written = writeEntry(move target, index, move carried)\n return ()\n}\n\nfn entryHash(entry: &Entry) -> u64 {\n return entry.hash\n}\n\nfn writeEntry(owned: &mut Table, index: usize, entry: Entry) -> () {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let written = Slot.write>(move selected, move entry)\n }\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = OCCUPIED\n return ()\n}\n\nfn vacantAt(owned: &Table, index: usize) -> bool {\n let seen = RawBuffer.view(&owned.states, index, usize.ONE)\n return seen[usize.ZERO] == VACANT\n}\n\n// Replaces the entry at one slot, answering with the value the slot held. The key that arrives wins\n// and the key the slot held is released, so exactly one of the two equivalent keys survives.\nfn exchange(owned: &mut Table, index: usize, entry: Entry) -> Option {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let taken = Slot.take(move selected)\n let target = RawBuffer.slot(&mut owned.entries, index)\n let written = Slot.write>(move target, move entry)\n return partValue(move taken)\n }\n return absurd>()\n}\n\n// Moves the entry out of one slot and answers with its value, releasing the key it held.\nfn evict(owned: &mut Table, index: usize) -> Option {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let taken = Slot.take(move selected)\n return partValue(move taken)\n }\n return absurd>()\n}\n\nfn partValue(entry: Entry) -> Option {\n return match move entry {\n Entry { hash, key, value } => releaseKey(move key, move value)\n }\n}\n\nfn releaseKey(key: K, value: V) -> Option {\n drop key\n return Option.some(move value)\n}\n\nfn markRemoved(owned: &mut Table, index: usize) -> () {\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = REMOVED\n return ()\n}\n\n/// Inserts one owned key and value, answering with the value an equivalent key already held.\n///\n/// # Details\n///\n/// The map takes ownership of both. When an equivalent key is already present the map's length does\n/// not change, the replaced value travels to the caller, and the key the map held is released.\n///\n/// Fails only with `OutOfMemoryError`, and only from the growth this insert needed. A failed insert\n/// leaves every prior entry at its own key, and leaves the length and the bucket count unchanged.\npub effect fn insert(\n self: &mut HashMap,\n key: K,\n value: V\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n let hashed = HashKey.hash(&key, &self.seed)\n if usize.ZERO < self.capacity {\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.entries, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if found < self.capacity {\n let previous = exchange(\n &mut owned,\n found,\n Entry { hash: hashed, key: move key, value: move value },\n )\n self.storage = move owned\n return move previous\n }\n self.storage = move owned\n }\n // The key is absent. Growth is allocated before the storage is opened, so a failure here never\n // observes the map with its buffers taken out.\n if needsRoom(self.used, self.capacity) {\n let next = grownWidth(self.capacity)\n let fresh = run allocateTable(next)\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n self.storage = migrate(move storage, self.capacity, move fresh, next)\n self.capacity = next\n self.used = self.length\n }\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut index = bucketOf(hashed, self.capacity)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n if seen[index] == OCCUPIED {\n index = advance(index, self.capacity)\n } else {\n searching = false\n }\n }\n let fresh = vacantAt(&owned, index)\n let written = writeEntry(\n &mut owned,\n index,\n Entry { hash: hashed, key: move key, value: move value },\n )\n self.storage = move owned\n self.length = self.length + usize.ONE\n if fresh {\n self.used = self.used + usize.ONE\n }\n return Option.none()\n}\n\n/// Reports whether the map holds an entry under a key equivalent to one probe key.\n///\n/// # Details\n///\n/// This function consumes the probe key. It does not change the map or move a stored entry.\npub fn contains(self: &HashMap, key: K) -> bool {\n if self.capacity == usize.ZERO {\n return false\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return false\n }\n if state == OCCUPIED {\n let held = entryAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n return true\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return false\n}\n\n/// Returns the bucket holding an entry under a key equivalent to one probe key, or an absent value.\n///\n/// # Details\n///\n/// This is the lookup a map with move-only values answers: the bucket names the entry without\n/// moving anything out of the map. A move-only value can then be transferred with [`remove`].\n/// This function consumes the probe key.\npub fn indexOf(self: &HashMap, key: K) -> Option {\n if self.capacity == usize.ZERO {\n return Option.none()\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return Option.none()\n }\n if state == OCCUPIED {\n let held = entryAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n return Option.some(index)\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return Option.none()\n}\n\n/// Returns the value held under a key equivalent to one probe key, or an absent value.\n///\n/// # Details\n///\n/// Reads a complete entry copy, so it answers only when both stored key and value types are `Copy`.\n/// Use [`indexOf`] for a non-moving presence check and [`remove`] to transfer a move-only value.\n/// This function consumes the probe key and does not change the map.\npub fn get(self: &HashMap, key: K) -> Option {\n if self.capacity == usize.ZERO {\n return Option.none()\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return Option.none()\n }\n if state == OCCUPIED {\n let held = entryAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n return partValue(readEntry(self, index))\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return Option.none()\n}\n\n/// Runs one take-once callback with exclusive access to an existing value.\n///\n/// # Details\n///\n/// Lookup and mutation allocate nothing and never grow the map. Returns `true` after running the\n/// callback exactly once for an equivalent key, or `false` without running it when the key is\n/// absent. The unit callback cannot return its value borrow, and a callback that may park is\n/// rejected.\n///\n/// This function consumes the probe key but leaves the stored key, length, used count, and bucket\n/// count unchanged.\npub fn withMut<\n K: HashKey,\n V,\n F: once fn(&mut V) -> () + Intrinsic.NonParking\n>(self: &mut HashMap, key: K, use: F) -> bool {\n if self.capacity == usize.ZERO {\n return false\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.entries, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if self.capacity <= found {\n self.storage = move owned\n return false\n }\n let mut held = RawBuffer.viewMut>(&mut owned.entries, found, usize.ONE)\n use(&mut held[usize.ZERO].value)\n self.storage = move owned\n return true\n}\n\n/// Removes the entry under a key equivalent to one probe key and answers with its value.\n///\n/// # Details\n///\n/// Ownership of the value passes to the caller; the map does not also release it. The key the map\n/// held is released, and the probe key is released as well.\npub fn remove(self: &mut HashMap, key: K) -> Option {\n if self.capacity == usize.ZERO {\n return Option.none()\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.entries, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if self.capacity <= found {\n self.storage = move owned\n return Option.none()\n }\n let carried = evict(&mut owned, found)\n let marked = markRemoved(&mut owned, found)\n self.storage = move owned\n self.length = self.length - usize.ONE\n return move carried\n}\n\n/// Returns the key held in one bucket. Traps on a bucket that holds no entry.\n///\n/// # Details\n///\n/// Reads a complete entry copy, so both stored key and value types must be `Copy`.\n///\n/// # Gotchas\n///\n/// If `index` is out of range or [`occupiedAt`] returns `false`, the program traps.\npub fn keyAt(self: &HashMap, index: usize) -> K {\n if !occupiedAt(self, index) {\n let boom = 1 / 0\n }\n return partKey(readEntry(self, index))\n}\n\n/// Returns the value held in one bucket. Traps on a bucket that holds no entry.\n///\n/// # Details\n///\n/// Reads a complete entry copy, so both stored key and value types must be `Copy`.\n///\n/// # Gotchas\n///\n/// If `index` is out of range or [`occupiedAt`] returns `false`, the program traps.\npub fn valueAt(self: &HashMap, index: usize) -> V {\n if !occupiedAt(self, index) {\n let boom = 1 / 0\n }\n return unwrapValue(readEntry(self, index))\n}\n\nfn readEntry(self: &HashMap, index: usize) -> Entry {\n return match &self.storage {\n Unallocated nothing => absurd>()\n Table { entries, states } => readAt(&entries, index)\n }\n}\n\nfn readAt(entries: &RawBuffer>, index: usize) -> Entry {\n unsafe {\n return RawBuffer.read>(entries, index)\n }\n return absurd>()\n}\n\nfn partKey(entry: Entry) -> K {\n return match move entry {\n Entry { hash, key, value } => keepKey(move key, move value)\n }\n}\n\nfn keepKey(key: K, value: V) -> K {\n drop value\n return move key\n}\n\nfn unwrapValue(entry: Entry) -> V {\n return match move entry {\n Entry { hash, key, value } => keepValue(move key, move value)\n }\n}\n\nfn keepValue(key: K, value: V) -> V {\n drop key\n return move value\n}\n", + "//! Owned key-value storage with deterministic seeded hashing and open-addressed lookup.\n//!\n//! # When to use\n//! Use [`HashMap`] for key-based lookup when a key has a [`HashKey`] witness. Use\n//! `silk.vector.Vector` when presentation order should follow insertion, or when indexing rather\n//! than equivalence is the primary operation.\n//!\n//! # Details\n//! The map starts allocation-free, first allocates eight buckets, and grows before used buckets\n//! exceed three quarters of the table. Linear probing crosses removal markers; growth doubles the\n//! table, moves every live entry, and discards those markers. Allocation completes before the map\n//! commits, so failed growth leaves existing entries, length, and bucket count unchanged.\n//!\n//! One [`HashSeed`] plus the same operation sequence fixes bucket presentation order across the\n//! evaluator, native code, and WebAssembly. Iterate deterministically by scanning\n//! `0..bucketCount`, testing [`occupiedAt`], then reading [`keyAt`] and [`valueAt`]. This is bucket\n//! order, not insertion order.\n//!\n//! # Gotchas\n//! Lookup and removal consume the probe key. [`get`], [`keyAt`], and [`valueAt`] copy a complete\n//! stored entry and therefore require both stored key and value to be `Copy`; use [`withMut`] to\n//! update a move-only value in place or [`remove`] to transfer it out. Equivalent keys must also\n//! obey the [`HashKey`] hash contract.\n//!\n//! # Examples\n//! ## Insert, read, and remove one value\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.hash as Hash\n//!\n//! import silk.hash_map as HashMap\n//!\n//! import silk.option as Option\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut map = HashMap.make(Hash.seed(17))\n//! let inserting = HashMap.insert(&mut map, Hash.word(7), 42)\n//! |> Effect.provideMut(&mut allocator)\n//! let previous = run inserting\n//! drop previous\n//! let found = HashMap.get(&map, Hash.word(7))\n//! |> Option.unwrapOr(0)\n//! let removed = HashMap.remove(&mut map, Hash.word(7))\n//! drop removed\n//! if HashMap.contains(&map, Hash.word(7)) {\n//! return 0\n//! }\n//! return found\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\n// A hashed map over the allocation substrate. Ordinary Silk: no compiler phase knows this type, no\n// engine contains a hash operation, and every hash a program computes is a call to a function some\n// `HashKey` witness declared in Silk.\n//\n// Open addressing with linear probing over two buffers of one width: the entries, whose slot is\n// initialized exactly when its state byte says occupied, and the states — vacant, occupied, or\n// removed. A removed slot keeps a probe run intact after a removal, and growth drops the removed\n// marks because it rehomes only the occupied entries.\n//\n// Each entry carries the hash it was placed under. That is what lets growth rehome an entry without\n// hashing it again, which matters here beyond the saved work: a bound is not carried into a nested\n// generic call, so a `K: HashKey` body cannot hand its own `K` to another `K: HashKey` function.\n// Anything that needs the witness has to be written in the body that needs it. Storing the hash\n// keeps growth, placement, and release ordinary unbounded code, and leaves the witness reached from\n// exactly the five operations that take a key from the caller.\n//\n// Every bucket index is reduced from the hash in u64 arithmetic before it narrows to `usize`, so the\n// index a key lands on does not depend on the width of a pointer and the iteration order is the same\n// under the evaluator, under the native backend, and under WebAssembly.\n\nimport silk.bool as bool\nimport silk.allocator as AllocationFailure\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.hash { HashKey, HashSeed }\nimport silk.layout { Layout }\nimport silk.layout { LayoutOverflow }\nimport silk.option { Option, none, some }\nimport silk.raw_buffer as RawBuffer\nimport silk.slot as Slot\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A slot is vacant, and no key was ever placed here.\nconst VACANT: u8 = 0\n\n/// A slot holds an initialized entry.\nconst OCCUPIED: u8 = 1\n\n/// A slot held an entry that was removed. Probing continues through it; lookup does not stop.\nconst REMOVED: u8 = 2\n\n/// The width a map takes when it first allocates.\nconst INITIAL_WIDTH: usize = 8\n\n/// Internal key-value record exposed by the current table representation.\npub struct Entry {\n hash: u64\n key: K\n value: V\n}\n\nimpl Copy for Entry {}\n\n/// Allocation-free storage state used before the map creates its first table.\npub struct Unallocated {\n anchor: [Entry; 0]\n}\n\n/// Allocated entry and occupancy buffers used by [`HashMap`].\npub struct Table {\n entries: RawBuffer>\n states: RawBuffer\n}\n\n/// Owns unique keys and their values under one equivalence, hash witness, and seed.\n///\n/// # Details\n///\n/// An equivalent insertion replaces the stored value instead of adding another entry. The seed\n/// and operation sequence determine bucket presentation order, which is not insertion order.\npub struct HashMap {\n storage: Unallocated | Table\n seed: HashSeed\n length: usize\n used: usize\n capacity: usize\n}\n\n// Diverges on the arms the surrounding logic has already proven impossible.\nfn absurd() -> T {\n let boom = 1 / 0\n return absurd()\n}\n\n/// Constructs an empty map whose every hash is computed under one seed.\n///\n/// # Details\n///\n/// An empty map allocates nothing. The seed fixes the order the map will present its entries in,\n/// and is the only thing besides the sequence of operations that decides it.\npub fn make(seed: HashSeed) -> HashMap {\n return HashMap {\n storage: Unallocated { anchor: [] },\n seed: move seed,\n length: usize.ZERO,\n used: usize.ZERO,\n capacity: usize.ZERO,\n }\n}\n\n/// Returns the number of entries the map holds.\npub fn length(self: &HashMap) -> usize {\n return self.length\n}\n\n/// Returns the number of buckets the map presents, which is the range `occupiedAt` accepts.\npub fn bucketCount(self: &HashMap) -> usize {\n return self.capacity\n}\n\n/// Reports whether one bucket holds an entry. Out-of-range buckets hold nothing.\npub fn occupiedAt(self: &HashMap, index: usize) -> bool {\n if self.capacity <= index {\n return false\n }\n return stateAt(self, index) == OCCUPIED\n}\n\nfn stateAt(self: &HashMap, index: usize) -> u8 {\n return match &self.storage {\n Unallocated nothing => VACANT\n Table { entries, states } => firstByte(RawBuffer.view(&states, index, usize.ONE))\n }\n}\n\nfn firstByte(seen: &[u8]) -> u8 {\n return seen[usize.ZERO]\n}\n\nfn entryAt(self: &HashMap, index: usize) -> &[Entry] {\n return match &self.storage {\n Unallocated { anchor } => emptySlice(&anchor)\n Table { entries, states } => RawBuffer.view>(&entries, index, usize.ONE)\n }\n}\n\nfn emptySlice(anchor: &[Entry]) -> &[Entry] {\n return anchor\n}\n\n/// Reduces a hash to a bucket index in u64 arithmetic, so the index does not depend on the width of\n/// `usize` and one seed therefore fixes one order in every engine.\nfn bucketOf(hashed: u64, width: usize) -> usize {\n return u64.toUsize(u64.remainder(hashed, usize.toU64(width)))\n}\n\nfn advance(index: usize, width: usize) -> usize {\n let next = index + usize.ONE\n if next == width {\n return usize.ZERO\n }\n return next\n}\n\nimpl Drop for HashMap {\n fn drop(self: &mut HashMap) -> () {\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let width = Intrinsic.replace(self.capacity, usize.ZERO)\n let emptied = Intrinsic.replace(self.length, usize.ZERO)\n return match move storage {\n Unallocated nothing => ()\n Table full => releaseFull(move full, width)\n }\n }\n}\n\n// Releases every occupied entry exactly once and then both buffers, so a map dropped while\n// non-empty destroys the keys and values it still owns rather than leaking them.\nfn releaseFull(full: Table, width: usize) -> () {\n let mut owned = move full\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let cleared = Slot.dropValue(move selected)\n }\n index = index + usize.ONE\n }\n drop owned\n }\n return ()\n}\n\nfn openStorage(storage: Unallocated | Table) -> Table {\n return match move storage {\n Unallocated nothing => absurd>()\n Table full => move full\n }\n}\n\neffect fn overflowed() -> Layout ! OutOfMemoryError {\n return run AllocationFailure.outOfMemory()\n}\n\n// Both buffers exist before either is installed, so an allocation that fails releases the one\n// already taken and leaves the caller's map exactly as it was.\neffect fn allocateTable(count: usize) -> Table ! OutOfMemoryError ? &mut Allocator {\n let states = run vacantStates(count)\n let entries = run entryStorage(count)\n return Table { entries: move entries, states: move states }\n}\n\neffect fn vacantStates(count: usize) -> RawBuffer ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return markVacant(move allocation, count)\n}\n\nfn markVacant(allocation: Allocation, count: usize) -> RawBuffer {\n unsafe {\n let mut made = RawBuffer.from(move allocation, count)\n let mut index = usize.ZERO\n while index < count {\n let selected = RawBuffer.slot(&mut made, index)\n let written = Slot.write(move selected, VACANT)\n index = index + usize.ONE\n }\n return move made\n }\n return absurd>()\n}\n\neffect fn entryStorage(count: usize) -> RawBuffer> ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of>()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return adopt(move allocation, count)\n}\n\nfn adopt(allocation: Allocation, count: usize) -> RawBuffer> {\n unsafe {\n return RawBuffer.from>(move allocation, count)\n }\n return absurd>>()\n}\n\n/// Reports whether one more entry would take the map past three quarters of its buckets.\nfn needsRoom(used: usize, width: usize) -> bool {\n if width == usize.ZERO {\n return true\n }\n let quarter = width / (usize.ONE + usize.ONE + usize.ONE + usize.ONE)\n return width - quarter < used + usize.ONE\n}\n\nfn grownWidth(width: usize) -> usize {\n if width == usize.ZERO {\n return INITIAL_WIDTH\n }\n return width + width\n}\n\n// Rehomes every occupied entry into the replacement table under the hash it was placed with, then\n// releases the old buffers. The removed marks do not travel, so growth reclaims them.\nfn migrate(\n storage: Unallocated | Table,\n width: usize,\n fresh: Table,\n next: usize\n) -> Table {\n return match move storage {\n Unallocated nothing => move fresh\n Table full => rehome(move full, width, move fresh, next)\n }\n}\n\nfn rehome(old: Table, width: usize, fresh: Table, next: usize) -> Table {\n let mut source = move old\n let mut target = move fresh\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&source.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut source.entries, index)\n let carried = Slot.take(move selected)\n let placed = settle(&mut target, move carried, next)\n }\n index = index + usize.ONE\n }\n drop source\n }\n return move target\n}\n\n// Places one entry at the first slot its own probe run does not already occupy. Every slot of a\n// replacement table is vacant, so the run always ends.\nfn settle(target: &mut Table, carried: Entry, next: usize) -> () {\n let hashed = entryHash(&carried)\n let mut index = bucketOf(hashed, next)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&target.states, usize.ZERO, next)\n if seen[index] == OCCUPIED {\n index = advance(index, next)\n } else {\n searching = false\n }\n }\n let written = writeEntry(move target, index, move carried)\n return ()\n}\n\nfn entryHash(entry: &Entry) -> u64 {\n return entry.hash\n}\n\nfn writeEntry(owned: &mut Table, index: usize, entry: Entry) -> () {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let written = Slot.write>(move selected, move entry)\n }\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = OCCUPIED\n return ()\n}\n\nfn vacantAt(owned: &Table, index: usize) -> bool {\n let seen = RawBuffer.view(&owned.states, index, usize.ONE)\n return seen[usize.ZERO] == VACANT\n}\n\n// Replaces the entry at one slot, answering with the value the slot held. The key that arrives wins\n// and the key the slot held is released, so exactly one of the two equivalent keys survives.\nfn exchange(owned: &mut Table, index: usize, entry: Entry) -> Option {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let taken = Slot.take(move selected)\n let target = RawBuffer.slot(&mut owned.entries, index)\n let written = Slot.write>(move target, move entry)\n return partValue(move taken)\n }\n return absurd>()\n}\n\n// Moves the entry out of one slot and answers with its value, releasing the key it held.\nfn evict(owned: &mut Table, index: usize) -> Option {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.entries, index)\n let taken = Slot.take(move selected)\n return partValue(move taken)\n }\n return absurd>()\n}\n\nfn partValue(entry: Entry) -> Option {\n return match move entry {\n Entry { hash, key, value } => releaseKey(move key, move value)\n }\n}\n\nfn releaseKey(key: K, value: V) -> Option {\n drop key\n return some(move value)\n}\n\nfn markRemoved(owned: &mut Table, index: usize) -> () {\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = REMOVED\n return ()\n}\n\n/// Inserts one owned key and value, answering with the value an equivalent key already held.\n///\n/// # Details\n///\n/// The map takes ownership of both. When an equivalent key is already present the map's length does\n/// not change, the replaced value travels to the caller, and the key the map held is released.\n///\n/// Fails only with `OutOfMemoryError`, and only from the growth this insert needed. A failed insert\n/// leaves every prior entry at its own key, and leaves the length and the bucket count unchanged.\npub effect fn insert(\n self: &mut HashMap,\n key: K,\n value: V\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n let hashed = HashKey.hash(&key, &self.seed)\n if usize.ZERO < self.capacity {\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.entries, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if found < self.capacity {\n let previous = exchange(\n &mut owned,\n found,\n Entry { hash: hashed, key: move key, value: move value },\n )\n self.storage = move owned\n return move previous\n }\n self.storage = move owned\n }\n // The key is absent. Growth is allocated before the storage is opened, so a failure here never\n // observes the map with its buffers taken out.\n if needsRoom(self.used, self.capacity) {\n let next = grownWidth(self.capacity)\n let fresh = run allocateTable(next)\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n self.storage = migrate(move storage, self.capacity, move fresh, next)\n self.capacity = next\n self.used = self.length\n }\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut index = bucketOf(hashed, self.capacity)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n if seen[index] == OCCUPIED {\n index = advance(index, self.capacity)\n } else {\n searching = false\n }\n }\n let fresh = vacantAt(&owned, index)\n let written = writeEntry(\n &mut owned,\n index,\n Entry { hash: hashed, key: move key, value: move value },\n )\n self.storage = move owned\n self.length = self.length + usize.ONE\n if fresh {\n self.used = self.used + usize.ONE\n }\n return none()\n}\n\n/// Reports whether the map holds an entry under a key equivalent to one probe key.\n///\n/// # Details\n///\n/// This function consumes the probe key. It does not change the map or move a stored entry.\npub fn contains(self: &HashMap, key: K) -> bool {\n if self.capacity == usize.ZERO {\n return false\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return false\n }\n if state == OCCUPIED {\n let held = entryAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n return true\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return false\n}\n\n/// Returns the bucket holding an entry under a key equivalent to one probe key, or an absent value.\n///\n/// # Details\n///\n/// This is the lookup a map with move-only values answers: the bucket names the entry without\n/// moving anything out of the map. A move-only value can then be transferred with [`remove`].\n/// This function consumes the probe key.\npub fn indexOf(self: &HashMap, key: K) -> Option {\n if self.capacity == usize.ZERO {\n return none()\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return none()\n }\n if state == OCCUPIED {\n let held = entryAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n return some(index)\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return none()\n}\n\n/// Returns the value held under a key equivalent to one probe key, or an absent value.\n///\n/// # Details\n///\n/// Reads a complete entry copy, so it answers only when both stored key and value types are `Copy`.\n/// Use [`indexOf`] for a non-moving presence check and [`remove`] to transfer a move-only value.\n/// This function consumes the probe key and does not change the map.\npub fn get(self: &HashMap, key: K) -> Option {\n if self.capacity == usize.ZERO {\n return none()\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return none()\n }\n if state == OCCUPIED {\n let held = entryAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n return partValue(readEntry(self, index))\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return none()\n}\n\n/// Runs one take-once callback with exclusive access to an existing value.\n///\n/// # Details\n///\n/// Lookup and mutation allocate nothing and never grow the map. Returns `true` after running the\n/// callback exactly once for an equivalent key, or `false` without running it when the key is\n/// absent. The unit callback cannot return its value borrow, and a callback that may park is\n/// rejected.\n///\n/// This function consumes the probe key but leaves the stored key, length, used count, and bucket\n/// count unchanged.\npub fn withMut<\n K: HashKey,\n V,\n F: once fn(&mut V) -> () + Intrinsic.NonParking\n>(self: &mut HashMap, key: K, use: F) -> bool {\n if self.capacity == usize.ZERO {\n return false\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.entries, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if self.capacity <= found {\n self.storage = move owned\n return false\n }\n let mut held = RawBuffer.viewMut>(&mut owned.entries, found, usize.ONE)\n use(&mut held[usize.ZERO].value)\n self.storage = move owned\n return true\n}\n\n/// Removes the entry under a key equivalent to one probe key and answers with its value.\n///\n/// # Details\n///\n/// Ownership of the value passes to the caller; the map does not also release it. The key the map\n/// held is released, and the probe key is released as well.\npub fn remove(self: &mut HashMap, key: K) -> Option {\n if self.capacity == usize.ZERO {\n return none()\n }\n let hashed = HashKey.hash(&key, &self.seed)\n let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.entries, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].key) == (&key) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if self.capacity <= found {\n self.storage = move owned\n return none()\n }\n let carried = evict(&mut owned, found)\n let marked = markRemoved(&mut owned, found)\n self.storage = move owned\n self.length = self.length - usize.ONE\n return move carried\n}\n\n/// Returns the key held in one bucket. Traps on a bucket that holds no entry.\n///\n/// # Details\n///\n/// Reads a complete entry copy, so both stored key and value types must be `Copy`.\n///\n/// # Gotchas\n///\n/// If `index` is out of range or [`occupiedAt`] returns `false`, the program traps.\npub fn keyAt(self: &HashMap, index: usize) -> K {\n if !occupiedAt(self, index) {\n let boom = 1 / 0\n }\n return partKey(readEntry(self, index))\n}\n\n/// Returns the value held in one bucket. Traps on a bucket that holds no entry.\n///\n/// # Details\n///\n/// Reads a complete entry copy, so both stored key and value types must be `Copy`.\n///\n/// # Gotchas\n///\n/// If `index` is out of range or [`occupiedAt`] returns `false`, the program traps.\npub fn valueAt(self: &HashMap, index: usize) -> V {\n if !occupiedAt(self, index) {\n let boom = 1 / 0\n }\n return unwrapValue(readEntry(self, index))\n}\n\nfn readEntry(self: &HashMap, index: usize) -> Entry {\n return match &self.storage {\n Unallocated nothing => absurd>()\n Table { entries, states } => readAt(&entries, index)\n }\n}\n\nfn readAt(entries: &RawBuffer>, index: usize) -> Entry {\n unsafe {\n return RawBuffer.read>(entries, index)\n }\n return absurd>()\n}\n\nfn partKey(entry: Entry) -> K {\n return match move entry {\n Entry { hash, key, value } => keepKey(move key, move value)\n }\n}\n\nfn keepKey(key: K, value: V) -> K {\n drop value\n return move key\n}\n\nfn unwrapValue(entry: Entry) -> V {\n return match move entry {\n Entry { hash, key, value } => keepValue(move key, move value)\n }\n}\n\nfn keepValue(key: K, value: V) -> V {\n drop key\n return move value\n}\n", }, { module: 'silk/hash_set', path: 'silk/hash_set.silk', sourceIdentity: 'silk/hash_set', - digest: '115f10c03dd300637acd0d81ef463a42d4d2feba1dba36bf1b0e92f5d7a50dea', + digest: '796fe8d31f2fbee91a11539c8a7c01cf6320c349919ab14d96b6fc2ec092e940', documentation: 'silk/hash_set.silk', layer: 'portable', runtimeInventory: ['replace'], namespace: 'HashSet', source: - "//! Owned unique elements with deterministic seeded hashing and open-addressed membership lookup.\n//!\n//! # When to use\n//! Use [`HashSet`] when equivalence and fast membership are central and an element has a [`HashKey`]\n//! witness. Use `silk.vector.Vector` when order or duplicate values are part of the data.\n//!\n//! # Details\n//! The set starts allocation-free, first allocates eight buckets, and grows before used buckets\n//! exceed three quarters of the table. Linear probing crosses removal markers; growth doubles the\n//! table, moves every live element, and discards those markers. Allocation completes before commit,\n//! so failed growth leaves prior membership, length, and bucket count unchanged.\n//!\n//! One [`HashSeed`] plus the same operation sequence fixes bucket presentation order on every\n//! engine. Iterate deterministically by scanning `0..bucketCount`, testing [`occupiedAt`], and then\n//! calling [`elementAt`]. This is bucket order, not insertion order.\n//!\n//! # Gotchas\n//! Probes are consumed. Inserting an equivalent element keeps the element already stored and drops\n//! the arrival; [`remove`] instead transfers the stored element to the caller. [`elementAt`] copies\n//! and therefore requires a `Copy` element; [`contains`] and [`indexOf`] can inspect membership for\n//! move-only elements without taking them out.\n//!\n//! # Examples\n//! ## Insert one unique element and remove it\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.hash as Hash\n//!\n//! import silk.hash_set as HashSet\n//!\n//! import silk.option as Option\n//!\n//! import silk.u64 as u64\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut set = HashSet.make(Hash.seed(17))\n//! let inserting = HashSet.insert(&mut set, Hash.word(42))\n//! |> Effect.provideMut(&mut allocator)\n//! let existed = run inserting\n//! if existed || !HashSet.contains(&set, Hash.word(42)) {\n//! return 0\n//! }\n//! let removed = HashSet.remove(&mut set, Hash.word(42))\n//! |> Option.unwrapOr(Hash.word(0))\n//! return u64.toI32(removed.value)\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\n// A hashed set over the allocation substrate, on the same table `hash_map.silk` describes: open\n// addressing with linear probing over an element buffer and a state buffer of one width, each\n// element carrying the hash it was placed under, and every bucket index reduced in u64 arithmetic\n// before it narrows to `usize`.\n//\n// The table is written out again rather than reached for, because a bound is not carried into a\n// nested generic call: a `T: HashKey` body cannot hand its own `T` to a `K: HashKey` function, so a\n// set cannot be a map whose values are empty. What the two share is the shape, not the code.\n\nimport silk.bool as bool\nimport silk.allocator as AllocationFailure\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.hash { HashKey, HashSeed }\nimport silk.layout { Layout }\nimport silk.layout { LayoutOverflow }\nimport silk.option { Option, Some, None }\nimport silk.raw_buffer as RawBuffer\nimport silk.slot as Slot\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A slot is vacant, and no element was ever placed here.\nconst VACANT: u8 = 0\n\n/// A slot holds an initialized element.\nconst OCCUPIED: u8 = 1\n\n/// A slot held an element that was removed. Probing continues through it.\nconst REMOVED: u8 = 2\n\n/// The width a set takes when it first allocates.\nconst INITIAL_WIDTH: usize = 8\n\n/// Internal hashed element record exposed by the current table representation.\npub struct Member {\n hash: u64\n value: T\n}\n\nimpl Copy for Member {}\n\n/// Allocation-free storage state used before the set creates its first table.\npub struct Unseeded {\n anchor: [Member; 0]\n}\n\n/// Allocated element and occupancy buffers used by [`HashSet`].\npub struct Slots {\n members: RawBuffer>\n states: RawBuffer\n}\n\n/// Owns one representative of each equivalence class under one hash witness and seed.\n///\n/// # Details\n///\n/// An equivalent insertion keeps the representative already stored. The seed and operation\n/// sequence determine bucket presentation order, which is not insertion order.\npub struct HashSet {\n storage: Unseeded | Slots\n seed: HashSeed\n length: usize\n used: usize\n capacity: usize\n}\n\n// Diverges on the arms the surrounding logic has already proven impossible.\nfn absurd() -> T {\n let boom = 1 / 0\n return absurd()\n}\n\n/// Creates an empty set whose every hash is computed under one seed.\n///\n/// # Details\n///\n/// An empty set allocates no storage. The seed fixes bucket order for the same operation sequence.\npub fn make(seed: HashSeed) -> HashSet {\n return HashSet {\n storage: Unseeded { anchor: [] },\n seed: move seed,\n length: usize.ZERO,\n used: usize.ZERO,\n capacity: usize.ZERO,\n }\n}\n\n/// Returns the number of elements the set holds.\npub fn length(self: &HashSet) -> usize {\n return self.length\n}\n\n/// Returns the number of buckets the set presents, which is the range `occupiedAt` accepts.\npub fn bucketCount(self: &HashSet) -> usize {\n return self.capacity\n}\n\n/// Reports whether one bucket holds an element. Out-of-range buckets hold nothing.\npub fn occupiedAt(self: &HashSet, index: usize) -> bool {\n if self.capacity <= index {\n return false\n }\n return stateAt(self, index) == OCCUPIED\n}\n\nfn stateAt(self: &HashSet, index: usize) -> u8 {\n return match &self.storage {\n Unseeded nothing => VACANT\n Slots { members, states } => firstByte(RawBuffer.view(&states, index, usize.ONE))\n }\n}\n\nfn firstByte(seen: &[u8]) -> u8 {\n return seen[usize.ZERO]\n}\n\nfn memberAt(self: &HashSet, index: usize) -> &[Member] {\n return match &self.storage {\n Unseeded { anchor } => emptySlice(&anchor)\n Slots { members, states } => RawBuffer.view>(&members, index, usize.ONE)\n }\n}\n\nfn emptySlice(anchor: &[Member]) -> &[Member] {\n return anchor\n}\n\n/// Reduces a hash to a bucket index in u64 arithmetic, so the index does not depend on the width of\n/// `usize` and one seed therefore fixes one order in every engine.\nfn bucketOf(hashed: u64, width: usize) -> usize {\n return u64.toUsize(u64.remainder(hashed, usize.toU64(width)))\n}\n\nfn advance(index: usize, width: usize) -> usize {\n let next = index + usize.ONE\n if next == width {\n return usize.ZERO\n }\n return next\n}\n\nimpl Drop for HashSet {\n fn drop(self: &mut HashSet) -> () {\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let width = Intrinsic.replace(self.capacity, usize.ZERO)\n let emptied = Intrinsic.replace(self.length, usize.ZERO)\n return match move storage {\n Unseeded nothing => ()\n Slots held => releaseSlots(move held, width)\n }\n }\n}\n\n// Releases every occupied element exactly once and then both buffers, so a set dropped while\n// non-empty destroys what it still owns rather than leaking it.\nfn releaseSlots(held: Slots, width: usize) -> () {\n let mut owned = move held\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut owned.members, index)\n let cleared = Slot.dropValue(move selected)\n }\n index = index + usize.ONE\n }\n drop owned\n }\n return ()\n}\n\nfn openStorage(storage: Unseeded | Slots) -> Slots {\n return match move storage {\n Unseeded nothing => absurd>()\n Slots held => move held\n }\n}\n\neffect fn overflowed() -> Layout ! OutOfMemoryError {\n return run AllocationFailure.outOfMemory()\n}\n\n// Both buffers exist before either is installed, so an allocation that fails releases the one\n// already taken and leaves the caller's set exactly as it was.\neffect fn allocateSlots(count: usize) -> Slots ! OutOfMemoryError ? &mut Allocator {\n let states = run vacantStates(count)\n let members = run memberStorage(count)\n return Slots { members: move members, states: move states }\n}\n\neffect fn vacantStates(count: usize) -> RawBuffer ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return markVacant(move allocation, count)\n}\n\nfn markVacant(allocation: Allocation, count: usize) -> RawBuffer {\n unsafe {\n let mut made = RawBuffer.from(move allocation, count)\n let mut index = usize.ZERO\n while index < count {\n let selected = RawBuffer.slot(&mut made, index)\n let written = Slot.write(move selected, VACANT)\n index = index + usize.ONE\n }\n return move made\n }\n return absurd>()\n}\n\neffect fn memberStorage(count: usize) -> RawBuffer> ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of>()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return adopt(move allocation, count)\n}\n\nfn adopt(allocation: Allocation, count: usize) -> RawBuffer> {\n unsafe {\n return RawBuffer.from>(move allocation, count)\n }\n return absurd>>()\n}\n\n/// Reports whether one more element would take the set past three quarters of its buckets.\nfn needsRoom(used: usize, width: usize) -> bool {\n if width == usize.ZERO {\n return true\n }\n let quarter = width / (usize.ONE + usize.ONE + usize.ONE + usize.ONE)\n return width - quarter < used + usize.ONE\n}\n\nfn grownWidth(width: usize) -> usize {\n if width == usize.ZERO {\n return INITIAL_WIDTH\n }\n return width + width\n}\n\n// Rehomes every occupied element into the replacement table under the hash it was placed with, then\n// releases the old buffers. The removed marks do not travel, so growth reclaims them.\nfn migrate(\n storage: Unseeded | Slots,\n width: usize,\n fresh: Slots,\n next: usize\n) -> Slots {\n return match move storage {\n Unseeded nothing => move fresh\n Slots held => rehome(move held, width, move fresh, next)\n }\n}\n\nfn rehome(old: Slots, width: usize, fresh: Slots, next: usize) -> Slots {\n let mut source = move old\n let mut target = move fresh\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&source.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut source.members, index)\n let carried = Slot.take(move selected)\n let placed = settle(&mut target, move carried, next)\n }\n index = index + usize.ONE\n }\n drop source\n }\n return move target\n}\n\n// Places one element at the first slot its own probe run does not already occupy. Every slot of a\n// replacement table is vacant, so the run always ends.\nfn settle(target: &mut Slots, carried: Member, next: usize) -> () {\n let hashed = memberHash(&carried)\n let mut index = bucketOf(hashed, next)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&target.states, usize.ZERO, next)\n if seen[index] == OCCUPIED {\n index = advance(index, next)\n } else {\n searching = false\n }\n }\n let written = writeMember(move target, index, move carried)\n return ()\n}\n\nfn memberHash(member: &Member) -> u64 {\n return member.hash\n}\n\nfn writeMember(owned: &mut Slots, index: usize, member: Member) -> () {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.members, index)\n let written = Slot.write>(move selected, move member)\n }\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = OCCUPIED\n return ()\n}\n\nfn vacantAt(owned: &Slots, index: usize) -> bool {\n let seen = RawBuffer.view(&owned.states, index, usize.ONE)\n return seen[usize.ZERO] == VACANT\n}\n\nfn markRemoved(owned: &mut Slots, index: usize) -> () {\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = REMOVED\n return ()\n}\n\nfn release(value: T) -> () {\n drop value\n return ()\n}\n\nfn keep(member: Member) -> T {\n return match move member {\n Member { hash, value } => move value\n }\n}\n\n/// Inserts one owned element, reporting whether an equivalent element was already held.\n///\n/// # Details\n///\n/// A set never holds two equivalent elements. When one is already held the set is unchanged and the\n/// arriving element is released, so the element that survives is the one the set already had.\n///\n/// Fails only with `OutOfMemoryError`, and only from the growth this insert needed. A failed insert\n/// leaves every prior element present, and leaves the length and the bucket count unchanged.\npub effect fn insert(\n self: &mut HashSet,\n value: T\n) -> bool ! OutOfMemoryError ? &mut Allocator {\n let hashed = HashKey.hash(&value, &self.seed)\n if usize.ZERO < self.capacity {\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = false\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.members, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n found = true\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n self.storage = move owned\n if found {\n let released = release(move value)\n return true\n }\n }\n // The element is absent. Growth is allocated before the storage is opened, so a failure here\n // never observes the set with its buffers taken out.\n if needsRoom(self.used, self.capacity) {\n let next = grownWidth(self.capacity)\n let fresh = run allocateSlots(next)\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n self.storage = migrate(move storage, self.capacity, move fresh, next)\n self.capacity = next\n self.used = self.length\n }\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut index = bucketOf(hashed, self.capacity)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n if seen[index] == OCCUPIED {\n index = advance(index, self.capacity)\n } else {\n searching = false\n }\n }\n let fresh = vacantAt(&owned, index)\n let written = writeMember(&mut owned, index, Member { hash: hashed, value: move value })\n self.storage = move owned\n self.length = self.length + usize.ONE\n if fresh {\n self.used = self.used + usize.ONE\n }\n return false\n}\n\n/// Reports whether the set holds an element equivalent to one probe element.\n///\n/// # Details\n///\n/// This function consumes the probe element. It does not change the set or move a stored element.\npub fn contains(self: &HashSet, value: T) -> bool {\n if self.capacity == usize.ZERO {\n return false\n }\n let hashed = HashKey.hash(&value, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return false\n }\n if state == OCCUPIED {\n let held = memberAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n return true\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return false\n}\n\n/// Returns the bucket holding an element equivalent to one probe element, or an absent value.\n///\n/// # Details\n///\n/// This is the membership question a set of move-only elements answers without moving anything.\n/// This function consumes the probe element.\npub fn indexOf(self: &HashSet, value: T) -> Option {\n if self.capacity == usize.ZERO {\n return Option.none()\n }\n let hashed = HashKey.hash(&value, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return Option.none()\n }\n if state == OCCUPIED {\n let held = memberAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n return Option.some(index)\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return Option.none()\n}\n\n/// Removes the element equivalent to one probe element and answers with it.\n///\n/// # Details\n///\n/// Ownership passes to the caller; the set does not also release it. The probe element is released.\npub fn remove(self: &mut HashSet, value: T) -> Option {\n if self.capacity == usize.ZERO {\n return Option.none()\n }\n let hashed = HashKey.hash(&value, &self.seed)\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.members, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if self.capacity <= found {\n self.storage = move owned\n return Option.none()\n }\n let carried = evict(&mut owned, found)\n let marked = markRemoved(&mut owned, found)\n self.storage = move owned\n self.length = self.length - usize.ONE\n return Option.some(move carried)\n}\n\nfn evict(owned: &mut Slots, index: usize) -> T {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.members, index)\n let taken = Slot.take(move selected)\n return keep(move taken)\n }\n return absurd()\n}\n\n/// Returns the element held in one bucket. Traps on a bucket that holds no element.\n///\n/// # Details\n///\n/// Reads a copy out of the set, so it answers for a set whose element type is `Copy`.\n///\n/// # Gotchas\n///\n/// If `index` is out of range or [`occupiedAt`] returns `false`, the program traps.\npub fn elementAt(self: &HashSet, index: usize) -> T {\n if !occupiedAt(self, index) {\n let boom = 1 / 0\n }\n return keep(readMember(self, index))\n}\n\nfn readMember(self: &HashSet, index: usize) -> Member {\n return match &self.storage {\n Unseeded nothing => absurd>()\n Slots { members, states } => readAt(&members, index)\n }\n}\n\nfn readAt(members: &RawBuffer>, index: usize) -> Member {\n unsafe {\n return RawBuffer.read>(members, index)\n }\n return absurd>()\n}\n", + "//! Owned unique elements with deterministic seeded hashing and open-addressed membership lookup.\n//!\n//! # When to use\n//! Use [`HashSet`] when equivalence and fast membership are central and an element has a [`HashKey`]\n//! witness. Use `silk.vector.Vector` when order or duplicate values are part of the data.\n//!\n//! # Details\n//! The set starts allocation-free, first allocates eight buckets, and grows before used buckets\n//! exceed three quarters of the table. Linear probing crosses removal markers; growth doubles the\n//! table, moves every live element, and discards those markers. Allocation completes before commit,\n//! so failed growth leaves prior membership, length, and bucket count unchanged.\n//!\n//! One [`HashSeed`] plus the same operation sequence fixes bucket presentation order on every\n//! engine. Iterate deterministically by scanning `0..bucketCount`, testing [`occupiedAt`], and then\n//! calling [`elementAt`]. This is bucket order, not insertion order.\n//!\n//! # Gotchas\n//! Probes are consumed. Inserting an equivalent element keeps the element already stored and drops\n//! the arrival; [`remove`] instead transfers the stored element to the caller. [`elementAt`] copies\n//! and therefore requires a `Copy` element; [`contains`] and [`indexOf`] can inspect membership for\n//! move-only elements without taking them out.\n//!\n//! # Examples\n//! ## Insert one unique element and remove it\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.hash as Hash\n//!\n//! import silk.hash_set as HashSet\n//!\n//! import silk.option as Option\n//!\n//! import silk.u64 as u64\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut set = HashSet.make(Hash.seed(17))\n//! let inserting = HashSet.insert(&mut set, Hash.word(42))\n//! |> Effect.provideMut(&mut allocator)\n//! let existed = run inserting\n//! if existed || !HashSet.contains(&set, Hash.word(42)) {\n//! return 0\n//! }\n//! let removed = HashSet.remove(&mut set, Hash.word(42))\n//! |> Option.unwrapOr(Hash.word(0))\n//! return u64.toI32(removed.value)\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\n// A hashed set over the allocation substrate, on the same table `hash_map.silk` describes: open\n// addressing with linear probing over an element buffer and a state buffer of one width, each\n// element carrying the hash it was placed under, and every bucket index reduced in u64 arithmetic\n// before it narrows to `usize`.\n//\n// The table is written out again rather than reached for, because a bound is not carried into a\n// nested generic call: a `T: HashKey` body cannot hand its own `T` to a `K: HashKey` function, so a\n// set cannot be a map whose values are empty. What the two share is the shape, not the code.\n\nimport silk.bool as bool\nimport silk.allocator as AllocationFailure\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.hash { HashKey, HashSeed }\nimport silk.layout { Layout }\nimport silk.layout { LayoutOverflow }\nimport silk.option { Option, none, some }\nimport silk.raw_buffer as RawBuffer\nimport silk.slot as Slot\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A slot is vacant, and no element was ever placed here.\nconst VACANT: u8 = 0\n\n/// A slot holds an initialized element.\nconst OCCUPIED: u8 = 1\n\n/// A slot held an element that was removed. Probing continues through it.\nconst REMOVED: u8 = 2\n\n/// The width a set takes when it first allocates.\nconst INITIAL_WIDTH: usize = 8\n\n/// Internal hashed element record exposed by the current table representation.\npub struct Member {\n hash: u64\n value: T\n}\n\nimpl Copy for Member {}\n\n/// Allocation-free storage state used before the set creates its first table.\npub struct Unseeded {\n anchor: [Member; 0]\n}\n\n/// Allocated element and occupancy buffers used by [`HashSet`].\npub struct Slots {\n members: RawBuffer>\n states: RawBuffer\n}\n\n/// Owns one representative of each equivalence class under one hash witness and seed.\n///\n/// # Details\n///\n/// An equivalent insertion keeps the representative already stored. The seed and operation\n/// sequence determine bucket presentation order, which is not insertion order.\npub struct HashSet {\n storage: Unseeded | Slots\n seed: HashSeed\n length: usize\n used: usize\n capacity: usize\n}\n\n// Diverges on the arms the surrounding logic has already proven impossible.\nfn absurd() -> T {\n let boom = 1 / 0\n return absurd()\n}\n\n/// Creates an empty set whose every hash is computed under one seed.\n///\n/// # Details\n///\n/// An empty set allocates no storage. The seed fixes bucket order for the same operation sequence.\npub fn make(seed: HashSeed) -> HashSet {\n return HashSet {\n storage: Unseeded { anchor: [] },\n seed: move seed,\n length: usize.ZERO,\n used: usize.ZERO,\n capacity: usize.ZERO,\n }\n}\n\n/// Returns the number of elements the set holds.\npub fn length(self: &HashSet) -> usize {\n return self.length\n}\n\n/// Returns the number of buckets the set presents, which is the range `occupiedAt` accepts.\npub fn bucketCount(self: &HashSet) -> usize {\n return self.capacity\n}\n\n/// Reports whether one bucket holds an element. Out-of-range buckets hold nothing.\npub fn occupiedAt(self: &HashSet, index: usize) -> bool {\n if self.capacity <= index {\n return false\n }\n return stateAt(self, index) == OCCUPIED\n}\n\nfn stateAt(self: &HashSet, index: usize) -> u8 {\n return match &self.storage {\n Unseeded nothing => VACANT\n Slots { members, states } => firstByte(RawBuffer.view(&states, index, usize.ONE))\n }\n}\n\nfn firstByte(seen: &[u8]) -> u8 {\n return seen[usize.ZERO]\n}\n\nfn memberAt(self: &HashSet, index: usize) -> &[Member] {\n return match &self.storage {\n Unseeded { anchor } => emptySlice(&anchor)\n Slots { members, states } => RawBuffer.view>(&members, index, usize.ONE)\n }\n}\n\nfn emptySlice(anchor: &[Member]) -> &[Member] {\n return anchor\n}\n\n/// Reduces a hash to a bucket index in u64 arithmetic, so the index does not depend on the width of\n/// `usize` and one seed therefore fixes one order in every engine.\nfn bucketOf(hashed: u64, width: usize) -> usize {\n return u64.toUsize(u64.remainder(hashed, usize.toU64(width)))\n}\n\nfn advance(index: usize, width: usize) -> usize {\n let next = index + usize.ONE\n if next == width {\n return usize.ZERO\n }\n return next\n}\n\nimpl Drop for HashSet {\n fn drop(self: &mut HashSet) -> () {\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let width = Intrinsic.replace(self.capacity, usize.ZERO)\n let emptied = Intrinsic.replace(self.length, usize.ZERO)\n return match move storage {\n Unseeded nothing => ()\n Slots held => releaseSlots(move held, width)\n }\n }\n}\n\n// Releases every occupied element exactly once and then both buffers, so a set dropped while\n// non-empty destroys what it still owns rather than leaking it.\nfn releaseSlots(held: Slots, width: usize) -> () {\n let mut owned = move held\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut owned.members, index)\n let cleared = Slot.dropValue(move selected)\n }\n index = index + usize.ONE\n }\n drop owned\n }\n return ()\n}\n\nfn openStorage(storage: Unseeded | Slots) -> Slots {\n return match move storage {\n Unseeded nothing => absurd>()\n Slots held => move held\n }\n}\n\neffect fn overflowed() -> Layout ! OutOfMemoryError {\n return run AllocationFailure.outOfMemory()\n}\n\n// Both buffers exist before either is installed, so an allocation that fails releases the one\n// already taken and leaves the caller's set exactly as it was.\neffect fn allocateSlots(count: usize) -> Slots ! OutOfMemoryError ? &mut Allocator {\n let states = run vacantStates(count)\n let members = run memberStorage(count)\n return Slots { members: move members, states: move states }\n}\n\neffect fn vacantStates(count: usize) -> RawBuffer ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return markVacant(move allocation, count)\n}\n\nfn markVacant(allocation: Allocation, count: usize) -> RawBuffer {\n unsafe {\n let mut made = RawBuffer.from(move allocation, count)\n let mut index = usize.ZERO\n while index < count {\n let selected = RawBuffer.slot(&mut made, index)\n let written = Slot.write(move selected, VACANT)\n index = index + usize.ONE\n }\n return move made\n }\n return absurd>()\n}\n\neffect fn memberStorage(count: usize) -> RawBuffer> ! OutOfMemoryError ? &mut Allocator {\n let element = Layout.of>()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n return adopt(move allocation, count)\n}\n\nfn adopt(allocation: Allocation, count: usize) -> RawBuffer> {\n unsafe {\n return RawBuffer.from>(move allocation, count)\n }\n return absurd>>()\n}\n\n/// Reports whether one more element would take the set past three quarters of its buckets.\nfn needsRoom(used: usize, width: usize) -> bool {\n if width == usize.ZERO {\n return true\n }\n let quarter = width / (usize.ONE + usize.ONE + usize.ONE + usize.ONE)\n return width - quarter < used + usize.ONE\n}\n\nfn grownWidth(width: usize) -> usize {\n if width == usize.ZERO {\n return INITIAL_WIDTH\n }\n return width + width\n}\n\n// Rehomes every occupied element into the replacement table under the hash it was placed with, then\n// releases the old buffers. The removed marks do not travel, so growth reclaims them.\nfn migrate(\n storage: Unseeded | Slots,\n width: usize,\n fresh: Slots,\n next: usize\n) -> Slots {\n return match move storage {\n Unseeded nothing => move fresh\n Slots held => rehome(move held, width, move fresh, next)\n }\n}\n\nfn rehome(old: Slots, width: usize, fresh: Slots, next: usize) -> Slots {\n let mut source = move old\n let mut target = move fresh\n unsafe {\n let mut index = usize.ZERO\n while index < width {\n let seen = RawBuffer.view(&source.states, usize.ZERO, width)\n if seen[index] == OCCUPIED {\n let selected = RawBuffer.slot(&mut source.members, index)\n let carried = Slot.take(move selected)\n let placed = settle(&mut target, move carried, next)\n }\n index = index + usize.ONE\n }\n drop source\n }\n return move target\n}\n\n// Places one element at the first slot its own probe run does not already occupy. Every slot of a\n// replacement table is vacant, so the run always ends.\nfn settle(target: &mut Slots, carried: Member, next: usize) -> () {\n let hashed = memberHash(&carried)\n let mut index = bucketOf(hashed, next)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&target.states, usize.ZERO, next)\n if seen[index] == OCCUPIED {\n index = advance(index, next)\n } else {\n searching = false\n }\n }\n let written = writeMember(move target, index, move carried)\n return ()\n}\n\nfn memberHash(member: &Member) -> u64 {\n return member.hash\n}\n\nfn writeMember(owned: &mut Slots, index: usize, member: Member) -> () {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.members, index)\n let written = Slot.write>(move selected, move member)\n }\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = OCCUPIED\n return ()\n}\n\nfn vacantAt(owned: &Slots, index: usize) -> bool {\n let seen = RawBuffer.view(&owned.states, index, usize.ONE)\n return seen[usize.ZERO] == VACANT\n}\n\nfn markRemoved(owned: &mut Slots, index: usize) -> () {\n let mut states = RawBuffer.viewMut(&mut owned.states, index, usize.ONE)\n states[usize.ZERO] = REMOVED\n return ()\n}\n\nfn release(value: T) -> () {\n drop value\n return ()\n}\n\nfn keep(member: Member) -> T {\n return match move member {\n Member { hash, value } => move value\n }\n}\n\n/// Inserts one owned element, reporting whether an equivalent element was already held.\n///\n/// # Details\n///\n/// A set never holds two equivalent elements. When one is already held the set is unchanged and the\n/// arriving element is released, so the element that survives is the one the set already had.\n///\n/// Fails only with `OutOfMemoryError`, and only from the growth this insert needed. A failed insert\n/// leaves every prior element present, and leaves the length and the bucket count unchanged.\npub effect fn insert(\n self: &mut HashSet,\n value: T\n) -> bool ! OutOfMemoryError ? &mut Allocator {\n let hashed = HashKey.hash(&value, &self.seed)\n if usize.ZERO < self.capacity {\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = false\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.members, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n found = true\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n self.storage = move owned\n if found {\n let released = release(move value)\n return true\n }\n }\n // The element is absent. Growth is allocated before the storage is opened, so a failure here\n // never observes the set with its buffers taken out.\n if needsRoom(self.used, self.capacity) {\n let next = grownWidth(self.capacity)\n let fresh = run allocateSlots(next)\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n self.storage = migrate(move storage, self.capacity, move fresh, next)\n self.capacity = next\n self.used = self.length\n }\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut index = bucketOf(hashed, self.capacity)\n let mut searching = true\n while searching {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n if seen[index] == OCCUPIED {\n index = advance(index, self.capacity)\n } else {\n searching = false\n }\n }\n let fresh = vacantAt(&owned, index)\n let written = writeMember(&mut owned, index, Member { hash: hashed, value: move value })\n self.storage = move owned\n self.length = self.length + usize.ONE\n if fresh {\n self.used = self.used + usize.ONE\n }\n return false\n}\n\n/// Reports whether the set holds an element equivalent to one probe element.\n///\n/// # Details\n///\n/// This function consumes the probe element. It does not change the set or move a stored element.\npub fn contains(self: &HashSet, value: T) -> bool {\n if self.capacity == usize.ZERO {\n return false\n }\n let hashed = HashKey.hash(&value, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return false\n }\n if state == OCCUPIED {\n let held = memberAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n return true\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return false\n}\n\n/// Returns the bucket holding an element equivalent to one probe element, or an absent value.\n///\n/// # Details\n///\n/// This is the membership question a set of move-only elements answers without moving anything.\n/// This function consumes the probe element.\npub fn indexOf(self: &HashSet, value: T) -> Option {\n if self.capacity == usize.ZERO {\n return none()\n }\n let hashed = HashKey.hash(&value, &self.seed)\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let state = stateAt(self, index)\n if state == VACANT {\n return none()\n }\n if state == OCCUPIED {\n let held = memberAt(self, index)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n return some(index)\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n return none()\n}\n\n/// Removes the element equivalent to one probe element and answers with it.\n///\n/// # Details\n///\n/// Ownership passes to the caller; the set does not also release it. The probe element is released.\npub fn remove(self: &mut HashSet, value: T) -> Option {\n if self.capacity == usize.ZERO {\n return none()\n }\n let hashed = HashKey.hash(&value, &self.seed)\n let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] })\n let mut owned = openStorage(move storage)\n let mut found = self.capacity\n let mut index = bucketOf(hashed, self.capacity)\n let mut scanned = usize.ZERO\n while scanned < self.capacity {\n let seen = RawBuffer.view(&owned.states, usize.ZERO, self.capacity)\n let state = seen[index]\n if state == VACANT {\n break\n }\n if state == OCCUPIED {\n let held = RawBuffer.view>(&owned.members, index, usize.ONE)\n if held[usize.ZERO].hash == hashed {\n if (&held[usize.ZERO].value) == (&value) {\n found = index\n break\n }\n }\n }\n index = advance(index, self.capacity)\n scanned = scanned + usize.ONE\n }\n if self.capacity <= found {\n self.storage = move owned\n return none()\n }\n let carried = evict(&mut owned, found)\n let marked = markRemoved(&mut owned, found)\n self.storage = move owned\n self.length = self.length - usize.ONE\n return some(move carried)\n}\n\nfn evict(owned: &mut Slots, index: usize) -> T {\n unsafe {\n let selected = RawBuffer.slot(&mut owned.members, index)\n let taken = Slot.take(move selected)\n return keep(move taken)\n }\n return absurd()\n}\n\n/// Returns the element held in one bucket. Traps on a bucket that holds no element.\n///\n/// # Details\n///\n/// Reads a copy out of the set, so it answers for a set whose element type is `Copy`.\n///\n/// # Gotchas\n///\n/// If `index` is out of range or [`occupiedAt`] returns `false`, the program traps.\npub fn elementAt(self: &HashSet, index: usize) -> T {\n if !occupiedAt(self, index) {\n let boom = 1 / 0\n }\n return keep(readMember(self, index))\n}\n\nfn readMember(self: &HashSet, index: usize) -> Member {\n return match &self.storage {\n Unseeded nothing => absurd>()\n Slots { members, states } => readAt(&members, index)\n }\n}\n\nfn readAt(members: &RawBuffer>, index: usize) -> Member {\n unsafe {\n return RawBuffer.read>(members, index)\n }\n return absurd>()\n}\n", }, { module: 'silk/host_input', path: 'silk/host_input.silk', sourceIdentity: 'silk/host_input', - digest: 'fca1cd82684d229fd1d86e979204798e542c9d31eedd9edb7c0db24a561b0c03', + digest: '3611713aedf152776ddd2f8bb571cca45c198f0786b1c644df4467a9eb7dc691', documentation: 'silk/host_input.silk', layer: 'portable', runtimeInventory: [], namespace: 'HostInput', aliases: ['HostInputError'], source: - '//! Explicit access to process arguments, environment values, and the working directory as bytes.\n//!\n//! # When to use\n//! Require [`HostInput`] when code needs launch-time process data but should remain replaceable in\n//! tests. Use [`text`] only when the caller wants a checked UTF-8 view; keep the original bytes for\n//! lossless pass-through.\n//!\n//! # Details\n//! Arguments include the program name at index zero and retain host order. A missing argument index\n//! or unset variable is [`None`], while [`HostInputError`] means the provider could not answer.\n//! Returned [`Bytes`] values are independently owned, so lookup operations also carry explicit\n//! [`OutOfMemoryError`] and [`Allocator`] channels.\n//!\n//! The service is read-only and never mutates environment variables or the process working\n//! directory. No ambient global is consulted after a provider is supplied.\n//!\n//! # Examples\n//! ## Read the argument count through an application provider\n//!\n//! ```silk\n//! import silk.bytes as Bytes\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.host_input as Host\n//!\n//! import silk.option as Option\n//!\n//! import silk.usize as usize\n//!\n//! struct FixedInput {}\n//!\n//! effect fn argumentCount(self: &mut FixedInput) -> usize\n//! ! Host.HostInputError {\n//! return usize.ONE\n//! }\n//!\n//! effect fn argument(self: &mut FixedInput, index: usize) -> Option.Option\n//! ! Host.HostInputError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! fail Host.inputFailure()\n//! }\n//!\n//! effect fn variable(self: &mut FixedInput, name: &[u8]) -> Option.Option\n//! ! Host.HostInputError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! fail Host.inputFailure()\n//! }\n//!\n//! effect fn workingDirectory(self: &mut FixedInput) -> Bytes.Bytes\n//! ! Host.HostInputError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! fail Host.inputFailure()\n//! }\n//!\n//! impl Host.HostInput for FixedInput {\n//! argumentCount: FixedInput.argumentCount\n//! argument: FixedInput.argument\n//! variable: FixedInput.variable\n//! workingDirectory: FixedInput.workingDirectory\n//! }\n//!\n//! effect fn program() -> i32\n//! ! Host.HostInputError {\n//! let mut provider = FixedInput {}\n//! let total = run Host.argumentCount()\n//! |> Effect.provideMut(&mut provider)\n//! if total != usize.ONE {\n//! return 1\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Host.HostInputError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\n// The portable process-input boundary. A program receives its command line, its environment, and\n// its working directory here, explicitly, and never through an ambient global.\n//\n// Every value this service hands back is raw bytes, exactly as the process received them. A POSIX\n// argument or environment value is an arbitrary byte string, and a program must be able to read one\n// and pass it through unchanged, so the byte view is the primary one. `text` is the checked,\n// fallible view for the common case; it never replaces or discards the bytes.\n\nimport silk.bytes { Bytes }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { None, Option, Some }\nimport silk.result { Result }\nimport silk.string { InvalidUtf8, fromUtf8 as stringFromUtf8, utf8Bytes as stringUtf8Bytes }\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector { Vector, append as vectorAppend, make as vectorMake }\n\n/// A typed failure from a host-input provider that could not answer a lookup.\npub struct HostInputError {}\n\n/// Creates a host-input failure for a provider that cannot complete a lookup.\npub fn inputFailure() -> HostInputError {\n return HostInputError {}\n}\n\n/// A portable read-only service for process arguments, environment values, and working directory.\n///\n/// # Details\n///\n/// The service reads and never writes: it has no operation that sets an environment variable or\n/// changes the working directory. An absent argument index and an unset variable name are `None`\n/// rather than typed failures, because absence is an ordinary answer; only a host that cannot\n/// answer at all is `HostInputError`.\n///\n/// Returned byte values are independently owned. Operations that return bytes therefore require an\n/// exclusive [`Allocator`] and can also fail with [`OutOfMemoryError`].\npub service HostInput {\n /// Returns the argument count, including the program name at index zero.\n ///\n /// # Details\n ///\n /// A provider that cannot inspect the process arguments fails with `HostInputError`.\n effect fn argumentCount() -> usize ! HostInputError ? &mut HostInput\n /// Copies one argument as raw bytes, or returns `None` when `index` is out of range.\n ///\n /// # Details\n ///\n /// The returned bytes preserve host order and do not require valid UTF-8. Provider lookup failure\n /// produces `HostInputError`; ownership allocation produces `OutOfMemoryError`.\n effect fn argument(\n index: usize\n ) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator\n /// Copies one environment value as raw bytes, or returns `None` when `name` is unset.\n ///\n /// # Details\n ///\n /// This operation does not change the environment. Provider lookup failure produces\n /// `HostInputError`; ownership allocation produces `OutOfMemoryError`.\n effect fn variable(\n name: &[u8]\n ) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator\n /// Copies the process working directory as raw bytes.\n ///\n /// # Details\n ///\n /// This operation does not change the directory. An unavailable host value produces\n /// `HostInputError`; ownership allocation produces `OutOfMemoryError`.\n effect fn workingDirectory() -> Bytes ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator\n}\n\n/// Returns the process argument count through the active [`HostInput`] provider.\n///\n/// # Details\n///\n/// The count includes the program name at index zero. Provider failure produces\n/// [`HostInputError`].\npub effect fn argumentCount() -> usize ! HostInputError ? &mut HostInput {\n return run HostInput.argumentCount()\n}\n\n/// Copies one process argument through the active [`HostInput`] provider.\n///\n/// # Details\n///\n/// Returns `None` when `index` is at or past [`argumentCount`]. The returned [`Bytes`] value is\n/// independently owned and can contain bytes that are not valid UTF-8.\npub effect fn argument(\n index: usize\n) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.argument(index)\n}\n\n/// Copies one environment value selected by a raw byte name.\n///\n/// # Details\n///\n/// Returns `None` when the name is unset. This operation reads the provider and does not modify the\n/// process environment. The returned [`Bytes`] value is independently owned.\npub effect fn variable(\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.variable(name)\n}\n\n/// Copies one environment value selected by a valid UTF-8 name.\n///\n/// # Details\n///\n/// This function borrows the UTF-8 encoding of `name` and delegates to [`variable`]. It returns\n/// `None` when the name is unset and independently owns a present value.\npub effect fn variableNamed(\n name: string\n) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.variable(stringUtf8Bytes(name))\n}\n\n/// Copies the process working directory through the active [`HostInput`] provider.\n///\n/// # Details\n///\n/// The returned [`Bytes`] value is independently owned and is not required to be valid UTF-8. This\n/// operation reads the directory and does not change it.\npub effect fn workingDirectory() -> Bytes ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.workingDirectory()\n}\n\neffect fn missingArgument() -> Bytes ! HostInputError {\n fail inputFailure()\n}\n\n/// Copies all process arguments into an owned vector in host order.\n///\n/// # When to use\n///\n/// Use this function when the caller needs the complete argument list. Use [`argument`] for one\n/// index without retaining all argument values.\n///\n/// # Details\n///\n/// A host that reports a count it cannot then supply is a broken host, so a missing index below the\n/// count is `HostInputError` rather than a silently shorter sequence.\n///\n/// Each argument and the result vector own their storage. The operation preserves the program name\n/// at index zero.\npub effect fn arguments(\n) -> Vector ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n let total = run HostInput.argumentCount()\n let mut collected = vectorMake()\n let mut index = usize.ZERO\n while index < total {\n let found = run HostInput.argument(index)\n let owned = match move found {\n Some { value: bytes } => move bytes\n None {} => run missingArgument()\n }\n let appended = run vectorAppend(&mut collected, move owned)\n index = index + usize.ONE\n }\n return move collected\n}\n\n/// Validates host bytes as UTF-8 and returns a borrowed textual view or [`InvalidUtf8`].\n///\n/// # When to use\n///\n/// Use this function only when the caller needs text. Keep byte-oriented code on the original\n/// slice so every host value can pass through unchanged.\n///\n/// # Details\n///\n/// Host input is not required to be UTF-8. Validation does not allocate or change `values`. A\n/// failure identifies invalid text while the original bytes remain available to the caller.\npub fn text(values: &[u8]) -> Result {\n return stringFromUtf8(values)\n}\n', + '//! Explicit access to process arguments, environment values, and the working directory as bytes.\n//!\n//! # When to use\n//! Require [`HostInput`] when code needs launch-time process data but should remain replaceable in\n//! tests. Use [`text`] only when the caller wants a checked UTF-8 view; keep the original bytes for\n//! lossless pass-through.\n//!\n//! # Details\n//! Arguments include the program name at index zero and retain host order. A missing argument index\n//! or unset variable is [`None`], while [`HostInputError`] means the provider could not answer.\n//! Returned [`Bytes`] values are independently owned, so lookup operations also carry explicit\n//! [`OutOfMemoryError`] and [`Allocator`] channels.\n//!\n//! The service is read-only and never mutates environment variables or the process working\n//! directory. No ambient global is consulted after a provider is supplied.\n//!\n//! # Examples\n//! ## Read the argument count through an application provider\n//!\n//! ```silk\n//! import silk.bytes as Bytes\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.host_input as Host\n//!\n//! import silk.option as Option\n//!\n//! import silk.usize as usize\n//!\n//! struct FixedInput {}\n//!\n//! effect fn argumentCount(self: &mut FixedInput) -> usize\n//! ! Host.HostInputError {\n//! return usize.ONE\n//! }\n//!\n//! effect fn argument(self: &mut FixedInput, index: usize) -> Option.Option\n//! ! Host.HostInputError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! fail Host.inputFailure()\n//! }\n//!\n//! effect fn variable(self: &mut FixedInput, name: &[u8]) -> Option.Option\n//! ! Host.HostInputError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! fail Host.inputFailure()\n//! }\n//!\n//! effect fn workingDirectory(self: &mut FixedInput) -> Bytes.Bytes\n//! ! Host.HostInputError | Allocator.OutOfMemoryError\n//! ? &mut Allocator {\n//! fail Host.inputFailure()\n//! }\n//!\n//! impl Host.HostInput for FixedInput {\n//! argumentCount: FixedInput.argumentCount\n//! argument: FixedInput.argument\n//! variable: FixedInput.variable\n//! workingDirectory: FixedInput.workingDirectory\n//! }\n//!\n//! effect fn program() -> i32\n//! ! Host.HostInputError {\n//! let mut provider = FixedInput {}\n//! let total = run Host.argumentCount()\n//! |> Effect.provideMut(&mut provider)\n//! if total != usize.ONE {\n//! return 1\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Host.HostInputError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\n// The portable process-input boundary. A program receives its command line, its environment, and\n// its working directory here, explicitly, and never through an ambient global.\n//\n// Every value this service hands back is raw bytes, exactly as the process received them. A POSIX\n// argument or environment value is an arbitrary byte string, and a program must be able to read one\n// and pass it through unchanged, so the byte view is the primary one. `text` is the checked,\n// fallible view for the common case; it never replaces or discards the bytes.\n\nimport silk.bytes { Bytes }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { InvalidUtf8, fromUtf8 as stringFromUtf8, utf8Bytes as stringUtf8Bytes }\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector { Vector, append as vectorAppend, make as vectorMake }\n\n/// A typed failure from a host-input provider that could not answer a lookup.\npub struct HostInputError {}\n\n/// Creates a host-input failure for a provider that cannot complete a lookup.\npub fn inputFailure() -> HostInputError {\n return HostInputError {}\n}\n\n/// A portable read-only service for process arguments, environment values, and working directory.\n///\n/// # Details\n///\n/// The service reads and never writes: it has no operation that sets an environment variable or\n/// changes the working directory. An absent argument index and an unset variable name are `None`\n/// rather than typed failures, because absence is an ordinary answer; only a host that cannot\n/// answer at all is `HostInputError`.\n///\n/// Returned byte values are independently owned. Operations that return bytes therefore require an\n/// exclusive [`Allocator`] and can also fail with [`OutOfMemoryError`].\npub service HostInput {\n /// Returns the argument count, including the program name at index zero.\n ///\n /// # Details\n ///\n /// A provider that cannot inspect the process arguments fails with `HostInputError`.\n effect fn argumentCount() -> usize ! HostInputError ? &mut HostInput\n /// Copies one argument as raw bytes, or returns `None` when `index` is out of range.\n ///\n /// # Details\n ///\n /// The returned bytes preserve host order and do not require valid UTF-8. Provider lookup failure\n /// produces `HostInputError`; ownership allocation produces `OutOfMemoryError`.\n effect fn argument(\n index: usize\n ) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator\n /// Copies one environment value as raw bytes, or returns `None` when `name` is unset.\n ///\n /// # Details\n ///\n /// This operation does not change the environment. Provider lookup failure produces\n /// `HostInputError`; ownership allocation produces `OutOfMemoryError`.\n effect fn variable(\n name: &[u8]\n ) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator\n /// Copies the process working directory as raw bytes.\n ///\n /// # Details\n ///\n /// This operation does not change the directory. An unavailable host value produces\n /// `HostInputError`; ownership allocation produces `OutOfMemoryError`.\n effect fn workingDirectory() -> Bytes ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator\n}\n\n/// Returns the process argument count through the active [`HostInput`] provider.\n///\n/// # Details\n///\n/// The count includes the program name at index zero. Provider failure produces\n/// [`HostInputError`].\npub effect fn argumentCount() -> usize ! HostInputError ? &mut HostInput {\n return run HostInput.argumentCount()\n}\n\n/// Copies one process argument through the active [`HostInput`] provider.\n///\n/// # Details\n///\n/// Returns `None` when `index` is at or past [`argumentCount`]. The returned [`Bytes`] value is\n/// independently owned and can contain bytes that are not valid UTF-8.\npub effect fn argument(\n index: usize\n) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.argument(index)\n}\n\n/// Copies one environment value selected by a raw byte name.\n///\n/// # Details\n///\n/// Returns `None` when the name is unset. This operation reads the provider and does not modify the\n/// process environment. The returned [`Bytes`] value is independently owned.\npub effect fn variable(\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.variable(name)\n}\n\n/// Copies one environment value selected by a valid UTF-8 name.\n///\n/// # Details\n///\n/// This function borrows the UTF-8 encoding of `name` and delegates to [`variable`]. It returns\n/// `None` when the name is unset and independently owns a present value.\npub effect fn variableNamed(\n name: string\n) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.variable(stringUtf8Bytes(name))\n}\n\n/// Copies the process working directory through the active [`HostInput`] provider.\n///\n/// # Details\n///\n/// The returned [`Bytes`] value is independently owned and is not required to be valid UTF-8. This\n/// operation reads the directory and does not change it.\npub effect fn workingDirectory() -> Bytes ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n return run HostInput.workingDirectory()\n}\n\neffect fn missingArgument() -> Bytes ! HostInputError {\n fail inputFailure()\n}\n\n/// Copies all process arguments into an owned vector in host order.\n///\n/// # When to use\n///\n/// Use this function when the caller needs the complete argument list. Use [`argument`] for one\n/// index without retaining all argument values.\n///\n/// # Details\n///\n/// A host that reports a count it cannot then supply is a broken host, so a missing index below the\n/// count is `HostInputError` rather than a silently shorter sequence.\n///\n/// Each argument and the result vector own their storage. The operation preserves the program name\n/// at index zero.\npub effect fn arguments(\n) -> Vector ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator {\n let total = run HostInput.argumentCount()\n let mut collected = vectorMake()\n let mut index = usize.ZERO\n while index < total {\n let found = run HostInput.argument(index)\n let owned = match move found {\n Option.Some { value: bytes } => move bytes\n Option.None => run missingArgument()\n }\n let appended = run vectorAppend(&mut collected, move owned)\n index = index + usize.ONE\n }\n return move collected\n}\n\n/// Validates host bytes as UTF-8 and returns a borrowed textual view or [`InvalidUtf8`].\n///\n/// # When to use\n///\n/// Use this function only when the caller needs text. Keep byte-oriented code on the original\n/// slice so every host value can pass through unchanged.\n///\n/// # Details\n///\n/// Host input is not required to be UTF-8. Validation does not allocate or change `values`. A\n/// failure identifies invalid text while the original bytes remain available to the caller.\npub fn text(values: &[u8]) -> Result {\n return stringFromUtf8(values)\n}\n', }, { module: 'silk/i16', path: 'silk/i16.silk', sourceIdentity: 'silk/i16', - digest: '861f6eddf562dd1702b2bbf04b83c8ccd6e70375c95ca24d22e5078292a12292', + digest: '97bf422d9bd38e75a724ff3128ade3623ba5da9d4153874bf876c323b4628aab', documentation: 'silk/i16.silk', layer: 'portable', runtimeInventory: [ @@ -403,13 +403,13 @@ export const modules = [ ], namespace: 'i16', source: - '//! Sixteen-bit signed integers with explicit overflow, conversion, and text policies.\n//!\n//! # When to use\n//! Use `i16` for data whose public representation is exactly sixteen signed bits, such as a binary\n//! field or compact sample. Use `i32` or `i64` when the range is not itself part of the contract.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! Choose `checked*` to receive [`Option`] on invalid arithmetic, `wrapping*` for arithmetic modulo\n//! 2^16, or `saturating*` to clamp at [`MIN`] and [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] requires a complete, in-range representation; [`toText`] returns newly\n//! allocated owned text.\n//!\n//! # Gotchas\n//! [`MIN`] cannot be represented as a positive i16, so ordinary [`negate`] traps for that value.\n//!\n//! # Examples\n//! ## Preserve the sign during a right shift\n//! ```silk\n//! import silk.i16 as i16\n//!\n//! pub fn main() -> i32 {\n//! let shifted = i16.shiftRight(-84, 1)\n//! return i16.toI32(i16.negate(shifted))\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i16` value.\npub const MAX: i16 = 32767\n\n/// The smallest `i16` value.\npub const MIN: i16 = -32768\n\n/// The fixed width of `i16`, in bits.\npub const BITS: u32 = 16\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i16) -> i16 {\n return Intrinsic.i16Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i16` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i16) -> i16 {\n return Intrinsic.i16WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i16` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i16) -> i16 {\n return Intrinsic.i16SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i16) -> u8 {\n return Intrinsic.i16ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i16) -> Option {\n return Intrinsic.i16CheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i16) -> u16 {\n return Intrinsic.i16ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i16) -> Option {\n return Intrinsic.i16CheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i16) -> u32 {\n return Intrinsic.i16ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i16) -> Option {\n return Intrinsic.i16CheckedToU32(value)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i16) -> u64 {\n return Intrinsic.i16ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i16) -> Option {\n return Intrinsic.i16CheckedToU64(value)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i16) -> usize {\n return Intrinsic.i16ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i16) -> Option {\n return Intrinsic.i16CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: i16) -> i8 {\n return Intrinsic.i16ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: i16) -> Option {\n return Intrinsic.i16CheckedToI8(value)\n}\n\n/// Returns `value` unchanged as `i16`. Use this function when generic conversion code\n/// can select `i16` as both source and destination.\npub fn toI16(value: i16) -> i16 {\n return Intrinsic.i16ToI16(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i16`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI16(value: i16) -> Option {\n return Intrinsic.i16CheckedToI16(value)\n}\n\n/// Converts `value` exactly to `i32`. Every `i16` value is representable.\npub fn toI32(value: i16) -> i32 {\n return Intrinsic.i16ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `i16` value is\n/// representable.\npub fn checkedToI32(value: i16) -> Option {\n return Intrinsic.i16CheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `i16` value is representable.\npub fn toI64(value: i16) -> i64 {\n return Intrinsic.i16ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `i16` value is\n/// representable.\npub fn checkedToI64(value: i16) -> Option {\n return Intrinsic.i16CheckedToI64(value)\n}\n\n/// Converts `value` exactly to `isize`. Every `i16` value is representable.\npub fn toIsize(value: i16) -> isize {\n return Intrinsic.i16ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `i16` value is\n/// representable.\npub fn checkedToIsize(value: i16) -> Option {\n return Intrinsic.i16CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i16) -> f32 {\n return Intrinsic.i16ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i16) -> f64 {\n return Intrinsic.i16ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i16` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i16` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i16` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i16, right: i16) -> i16 {\n return Intrinsic.i16BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i16, right: i16) -> i16 {\n return Intrinsic.i16BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i16, right: i16) -> i16 {\n return Intrinsic.i16BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i16) -> i16 {\n return Intrinsic.i16BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i16, right: i16) -> i16 {\n return Intrinsic.i16ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i16, right: i16) -> i16 {\n return Intrinsic.i16ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i16, right: i16) -> i16 {\n return Intrinsic.i16RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i16, right: i16) -> i16 {\n return Intrinsic.i16RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i16, right: i16) -> i16 {\n return Intrinsic.i16WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i16, right: i16) -> i16 {\n return Intrinsic.i16WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i16, right: i16) -> i16 {\n return Intrinsic.i16WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i16, right: i16) -> i16 {\n return Intrinsic.i16SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i16, right: i16) -> i16 {\n return Intrinsic.i16SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i16, right: i16) -> i16 {\n return Intrinsic.i16SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i16` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i16` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i16` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i16, right: i16) -> bool {\n return Intrinsic.i16Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i16, right: i16) -> bool {\n return Intrinsic.i16NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i16, right: i16) -> bool {\n return Intrinsic.i16LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i16, right: i16) -> bool {\n return Intrinsic.i16LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i16, right: i16) -> bool {\n return Intrinsic.i16GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i16, right: i16) -> bool {\n return Intrinsic.i16GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i16) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i16`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i16` range.\npub fn parse(text: string) -> Result {\n return Format.i16Value(text)\n}\n', + '//! Sixteen-bit signed integers with explicit overflow, conversion, and text policies.\n//!\n//! # When to use\n//! Use `i16` for data whose public representation is exactly sixteen signed bits, such as a binary\n//! field or compact sample. Use `i32` or `i64` when the range is not itself part of the contract.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! Choose `checked*` to receive [`Option`] on invalid arithmetic, `wrapping*` for arithmetic modulo\n//! 2^16, or `saturating*` to clamp at [`MIN`] and [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] requires a complete, in-range representation; [`toText`] returns newly\n//! allocated owned text.\n//!\n//! # Gotchas\n//! [`MIN`] cannot be represented as a positive i16, so ordinary [`negate`] traps for that value.\n//!\n//! # Examples\n//! ## Preserve the sign during a right shift\n//! ```silk\n//! import silk.i16 as i16\n//!\n//! pub fn main() -> i32 {\n//! let shifted = i16.shiftRight(-84, 1)\n//! return i16.toI32(i16.negate(shifted))\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i16` value.\npub const MAX: i16 = 32767\n\n/// The smallest `i16` value.\npub const MIN: i16 = -32768\n\n/// The fixed width of `i16`, in bits.\npub const BITS: u32 = 16\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i16) -> i16 {\n return Intrinsic.i16Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i16` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i16) -> i16 {\n return Intrinsic.i16WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i16` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i16) -> i16 {\n return Intrinsic.i16SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i16) -> u8 {\n return Intrinsic.i16ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i16) -> Option {\n return Intrinsic.i16CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i16) -> u16 {\n return Intrinsic.i16ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i16) -> Option {\n return Intrinsic.i16CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i16) -> u32 {\n return Intrinsic.i16ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i16) -> Option {\n return Intrinsic.i16CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i16) -> u64 {\n return Intrinsic.i16ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i16) -> Option {\n return Intrinsic.i16CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i16) -> usize {\n return Intrinsic.i16ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i16) -> Option {\n return Intrinsic.i16CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: i16) -> i8 {\n return Intrinsic.i16ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: i16) -> Option {\n return Intrinsic.i16CheckedToI8>(value, some, none)\n}\n\n/// Returns `value` unchanged as `i16`. Use this function when generic conversion code\n/// can select `i16` as both source and destination.\npub fn toI16(value: i16) -> i16 {\n return Intrinsic.i16ToI16(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i16`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI16(value: i16) -> Option {\n return Intrinsic.i16CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` exactly to `i32`. Every `i16` value is representable.\npub fn toI32(value: i16) -> i32 {\n return Intrinsic.i16ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `i16` value is\n/// representable.\npub fn checkedToI32(value: i16) -> Option {\n return Intrinsic.i16CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `i16` value is representable.\npub fn toI64(value: i16) -> i64 {\n return Intrinsic.i16ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `i16` value is\n/// representable.\npub fn checkedToI64(value: i16) -> Option {\n return Intrinsic.i16CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` exactly to `isize`. Every `i16` value is representable.\npub fn toIsize(value: i16) -> isize {\n return Intrinsic.i16ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `i16` value is\n/// representable.\npub fn checkedToIsize(value: i16) -> Option {\n return Intrinsic.i16CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i16) -> f32 {\n return Intrinsic.i16ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i16) -> f64 {\n return Intrinsic.i16ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i16` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i16` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i16` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i16, right: i16) -> i16 {\n return Intrinsic.i16Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i16, right: i16) -> i16 {\n return Intrinsic.i16BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i16, right: i16) -> i16 {\n return Intrinsic.i16BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i16, right: i16) -> i16 {\n return Intrinsic.i16BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i16) -> i16 {\n return Intrinsic.i16BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i16, right: i16) -> i16 {\n return Intrinsic.i16ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i16, right: i16) -> i16 {\n return Intrinsic.i16ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i16, right: i16) -> i16 {\n return Intrinsic.i16RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i16, right: i16) -> i16 {\n return Intrinsic.i16RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i16, right: i16) -> i16 {\n return Intrinsic.i16WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i16, right: i16) -> i16 {\n return Intrinsic.i16WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i16, right: i16) -> i16 {\n return Intrinsic.i16WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i16, right: i16) -> i16 {\n return Intrinsic.i16SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i16, right: i16) -> i16 {\n return Intrinsic.i16SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i16, right: i16) -> i16 {\n return Intrinsic.i16SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i16` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i16` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i16` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i16, right: i16) -> Option {\n return Intrinsic.i16CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i16, right: i16) -> bool {\n return Intrinsic.i16Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i16, right: i16) -> bool {\n return Intrinsic.i16NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i16, right: i16) -> bool {\n return Intrinsic.i16LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i16, right: i16) -> bool {\n return Intrinsic.i16LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i16, right: i16) -> bool {\n return Intrinsic.i16GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i16, right: i16) -> bool {\n return Intrinsic.i16GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i16) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i16`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i16` range.\npub fn parse(text: string) -> Result {\n return Format.i16Value(text)\n}\n', }, { module: 'silk/i32', path: 'silk/i32.silk', sourceIdentity: 'silk/i32', - digest: '38db50141b9d6badd0b3ebab3478072edfa3abcce954e347105e9ae88e2d01e8', + digest: '1a942f578ea1c8a94f932fe11fcaec64ef97e95139fc2c44ae2ba36f3c51db23', documentation: 'silk/i32.silk', layer: 'portable', runtimeInventory: [ @@ -471,13 +471,13 @@ export const modules = [ ], namespace: 'i32', source: - '//! Thirty-two-bit signed integers and the default type of context-free integer literals.\n//!\n//! # When to use\n//! Use `i32` for ordinary signed whole-number calculations whose range is known to fit, and when a\n//! stable 32-bit representation matters. Choose `isize` for pointer-sized offsets and `i64` for a\n//! wider fixed range.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` reports invalid arithmetic with [`Option`]; `wrapping*` computes modulo 2^32;\n//! `saturating*` clamps at [`MIN`] or [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] rejects trailing bytes and separates invalid syntax from out-of-range input.\n//! [`toText`] allocates an owned decimal string.\n//!\n//! # Gotchas\n//! [`MIN`] has no positive i32 counterpart. Use [`wrappingNegate`] or [`saturatingNegate`] for its\n//! negation boundary. Use [`checkedDivide`] when division by `-1` can receive [`MIN`].\n//!\n//! # Examples\n//! ## Select an overflow result explicitly\n//! ```silk\n//! import silk.i32 as i32\n//!\n//! import silk.option as Option\n//!\n//! pub fn main() -> i32 {\n//! let checked = i32.checkedAdd(i32.MAX, 1)\n//! let recovered = move checked\n//! |> Option.unwrapOr(42)\n//! if i32.wrappingAdd(i32.MAX, 1) != i32.MIN {\n//! return 1\n//! }\n//! if i32.saturatingAdd(i32.MAX, 1) != i32.MAX {\n//! return 2\n//! }\n//! return recovered\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i32` value.\npub const MAX: i32 = 2147483647\n\n/// The smallest `i32` value.\npub const MIN: i32 = -2147483648\n\n/// The fixed width of `i32`, in bits.\npub const BITS: u32 = 32\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i32) -> i32 {\n return Intrinsic.i32Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i32` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i32) -> i32 {\n return Intrinsic.i32WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i32` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i32) -> i32 {\n return Intrinsic.i32SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i32) -> u8 {\n return Intrinsic.i32ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i32) -> Option {\n return Intrinsic.i32CheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i32) -> u16 {\n return Intrinsic.i32ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i32) -> Option {\n return Intrinsic.i32CheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i32) -> u32 {\n return Intrinsic.i32ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i32) -> Option {\n return Intrinsic.i32CheckedToU32(value)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i32) -> u64 {\n return Intrinsic.i32ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i32) -> Option {\n return Intrinsic.i32CheckedToU64(value)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i32) -> usize {\n return Intrinsic.i32ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i32) -> Option {\n return Intrinsic.i32CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: i32) -> i8 {\n return Intrinsic.i32ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: i32) -> Option {\n return Intrinsic.i32CheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: i32) -> i16 {\n return Intrinsic.i32ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: i32) -> Option {\n return Intrinsic.i32CheckedToI16(value)\n}\n\n/// Returns `value` unchanged as `i32`. Use this function when generic conversion code\n/// can select `i32` as both source and destination.\npub fn toI32(value: i32) -> i32 {\n return Intrinsic.i32ToI32(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i32`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI32(value: i32) -> Option {\n return Intrinsic.i32CheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `i32` value is representable.\npub fn toI64(value: i32) -> i64 {\n return Intrinsic.i32ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `i32` value is\n/// representable.\npub fn checkedToI64(value: i32) -> Option {\n return Intrinsic.i32CheckedToI64(value)\n}\n\n/// Converts `value` exactly to `isize`. Every `i32` value is representable.\npub fn toIsize(value: i32) -> isize {\n return Intrinsic.i32ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `i32` value is\n/// representable.\npub fn checkedToIsize(value: i32) -> Option {\n return Intrinsic.i32CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i32) -> f32 {\n return Intrinsic.i32ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i32) -> f64 {\n return Intrinsic.i32ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i32` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i32` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i32` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i32, right: i32) -> i32 {\n return Intrinsic.i32BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i32, right: i32) -> i32 {\n return Intrinsic.i32BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i32, right: i32) -> i32 {\n return Intrinsic.i32BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i32) -> i32 {\n return Intrinsic.i32BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i32, right: i32) -> i32 {\n return Intrinsic.i32ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i32, right: i32) -> i32 {\n return Intrinsic.i32ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i32, right: i32) -> i32 {\n return Intrinsic.i32RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i32, right: i32) -> i32 {\n return Intrinsic.i32RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i32, right: i32) -> i32 {\n return Intrinsic.i32WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i32, right: i32) -> i32 {\n return Intrinsic.i32WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i32, right: i32) -> i32 {\n return Intrinsic.i32WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i32, right: i32) -> i32 {\n return Intrinsic.i32SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i32, right: i32) -> i32 {\n return Intrinsic.i32SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i32, right: i32) -> i32 {\n return Intrinsic.i32SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i32` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i32` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i32` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i32, right: i32) -> bool {\n return Intrinsic.i32Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i32, right: i32) -> bool {\n return Intrinsic.i32NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i32, right: i32) -> bool {\n return Intrinsic.i32LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i32, right: i32) -> bool {\n return Intrinsic.i32LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i32, right: i32) -> bool {\n return Intrinsic.i32GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i32, right: i32) -> bool {\n return Intrinsic.i32GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i32) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i32`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i32` range.\npub fn parse(text: string) -> Result {\n return Format.i32Value(text)\n}\n', + '//! Thirty-two-bit signed integers and the default type of context-free integer literals.\n//!\n//! # When to use\n//! Use `i32` for ordinary signed whole-number calculations whose range is known to fit, and when a\n//! stable 32-bit representation matters. Choose `isize` for pointer-sized offsets and `i64` for a\n//! wider fixed range.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` reports invalid arithmetic with [`Option`]; `wrapping*` computes modulo 2^32;\n//! `saturating*` clamps at [`MIN`] or [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] rejects trailing bytes and separates invalid syntax from out-of-range input.\n//! [`toText`] allocates an owned decimal string.\n//!\n//! # Gotchas\n//! [`MIN`] has no positive i32 counterpart. Use [`wrappingNegate`] or [`saturatingNegate`] for its\n//! negation boundary. Use [`checkedDivide`] when division by `-1` can receive [`MIN`].\n//!\n//! # Examples\n//! ## Select an overflow result explicitly\n//! ```silk\n//! import silk.i32 as i32\n//!\n//! import silk.option as Option\n//!\n//! pub fn main() -> i32 {\n//! let checked = i32.checkedAdd(i32.MAX, 1)\n//! let recovered = move checked\n//! |> Option.unwrapOr(42)\n//! if i32.wrappingAdd(i32.MAX, 1) != i32.MIN {\n//! return 1\n//! }\n//! if i32.saturatingAdd(i32.MAX, 1) != i32.MAX {\n//! return 2\n//! }\n//! return recovered\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i32` value.\npub const MAX: i32 = 2147483647\n\n/// The smallest `i32` value.\npub const MIN: i32 = -2147483648\n\n/// The fixed width of `i32`, in bits.\npub const BITS: u32 = 32\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i32) -> i32 {\n return Intrinsic.i32Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i32` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i32) -> i32 {\n return Intrinsic.i32WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i32` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i32) -> i32 {\n return Intrinsic.i32SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i32) -> u8 {\n return Intrinsic.i32ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i32) -> Option {\n return Intrinsic.i32CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i32) -> u16 {\n return Intrinsic.i32ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i32) -> Option {\n return Intrinsic.i32CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i32) -> u32 {\n return Intrinsic.i32ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i32) -> Option {\n return Intrinsic.i32CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i32) -> u64 {\n return Intrinsic.i32ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i32) -> Option {\n return Intrinsic.i32CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i32) -> usize {\n return Intrinsic.i32ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i32) -> Option {\n return Intrinsic.i32CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: i32) -> i8 {\n return Intrinsic.i32ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: i32) -> Option {\n return Intrinsic.i32CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: i32) -> i16 {\n return Intrinsic.i32ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: i32) -> Option {\n return Intrinsic.i32CheckedToI16>(value, some, none)\n}\n\n/// Returns `value` unchanged as `i32`. Use this function when generic conversion code\n/// can select `i32` as both source and destination.\npub fn toI32(value: i32) -> i32 {\n return Intrinsic.i32ToI32(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i32`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI32(value: i32) -> Option {\n return Intrinsic.i32CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `i32` value is representable.\npub fn toI64(value: i32) -> i64 {\n return Intrinsic.i32ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `i32` value is\n/// representable.\npub fn checkedToI64(value: i32) -> Option {\n return Intrinsic.i32CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` exactly to `isize`. Every `i32` value is representable.\npub fn toIsize(value: i32) -> isize {\n return Intrinsic.i32ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `i32` value is\n/// representable.\npub fn checkedToIsize(value: i32) -> Option {\n return Intrinsic.i32CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i32) -> f32 {\n return Intrinsic.i32ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i32) -> f64 {\n return Intrinsic.i32ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i32` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i32` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i32` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i32, right: i32) -> i32 {\n return Intrinsic.i32Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i32, right: i32) -> i32 {\n return Intrinsic.i32BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i32, right: i32) -> i32 {\n return Intrinsic.i32BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i32, right: i32) -> i32 {\n return Intrinsic.i32BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i32) -> i32 {\n return Intrinsic.i32BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i32, right: i32) -> i32 {\n return Intrinsic.i32ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i32, right: i32) -> i32 {\n return Intrinsic.i32ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i32, right: i32) -> i32 {\n return Intrinsic.i32RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i32, right: i32) -> i32 {\n return Intrinsic.i32RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i32, right: i32) -> i32 {\n return Intrinsic.i32WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i32, right: i32) -> i32 {\n return Intrinsic.i32WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i32, right: i32) -> i32 {\n return Intrinsic.i32WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i32, right: i32) -> i32 {\n return Intrinsic.i32SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i32, right: i32) -> i32 {\n return Intrinsic.i32SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i32, right: i32) -> i32 {\n return Intrinsic.i32SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i32` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i32` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i32` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i32, right: i32) -> Option {\n return Intrinsic.i32CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i32, right: i32) -> bool {\n return Intrinsic.i32Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i32, right: i32) -> bool {\n return Intrinsic.i32NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i32, right: i32) -> bool {\n return Intrinsic.i32LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i32, right: i32) -> bool {\n return Intrinsic.i32LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i32, right: i32) -> bool {\n return Intrinsic.i32GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i32, right: i32) -> bool {\n return Intrinsic.i32GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i32) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i32`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i32` range.\npub fn parse(text: string) -> Result {\n return Format.i32Value(text)\n}\n', }, { module: 'silk/i64', path: 'silk/i64.silk', sourceIdentity: 'silk/i64', - digest: 'b185f595546934105a8e648ebf530e4a5affb6841c3c459529a6e120909442d7', + digest: '3d3b141bdf6786e49dba74f26dca24f1e657e0203f13f05b054d6affba0923c9', documentation: 'silk/i64.silk', layer: 'portable', runtimeInventory: [ @@ -539,13 +539,13 @@ export const modules = [ ], namespace: 'i64', source: - '//! Sixty-four-bit signed integers for large fixed-width values and exact integer protocols.\n//!\n//! # When to use\n//! Use `i64` when a stable signed 64-bit range is part of storage, interchange, or arithmetic.\n//! Prefer `isize` only for target-sized offsets; its width changes with the compilation target.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`] instead, `wrapping*` uses arithmetic modulo 2^64, and `saturating*`\n//! clamps at [`MIN`] or [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] consumes the whole input and reports syntax separately from range overflow.\n//! [`toText`] allocates owned text. Conversion to `f32` or `f64` can round large exact integers.\n//!\n//! # Gotchas\n//! [`MIN`] has no positive i64 counterpart. Use [`wrappingNegate`] or [`saturatingNegate`] for its\n//! negation boundary. Use [`checkedDivide`] when division by `-1` can receive [`MIN`].\n//!\n//! # Examples\n//! ## Parse a complete signed decimal value\n//! ```silk\n//! import silk.format as Format\n//!\n//! import silk.i64 as i64\n//!\n//! import silk.result as Result\n//!\n//! pub fn main() -> i32 {\n//! let parsed = i64.parse("-42")\n//! let value = move parsed\n//! |> Result.unwrapOr(0)\n//! return i64.toI32(value + 84)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i64` value.\npub const MAX: i64 = 9223372036854775807\n\n/// The smallest `i64` value.\npub const MIN: i64 = -9223372036854775808\n\n/// The fixed width of `i64`, in bits.\npub const BITS: u32 = 64\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i64) -> i64 {\n return Intrinsic.i64Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i64` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i64) -> i64 {\n return Intrinsic.i64WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i64` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i64) -> i64 {\n return Intrinsic.i64SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i64) -> u8 {\n return Intrinsic.i64ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i64) -> Option {\n return Intrinsic.i64CheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i64) -> u16 {\n return Intrinsic.i64ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i64) -> Option {\n return Intrinsic.i64CheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i64) -> u32 {\n return Intrinsic.i64ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i64) -> Option {\n return Intrinsic.i64CheckedToU32(value)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i64) -> u64 {\n return Intrinsic.i64ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i64) -> Option {\n return Intrinsic.i64CheckedToU64(value)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i64) -> usize {\n return Intrinsic.i64ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i64) -> Option {\n return Intrinsic.i64CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: i64) -> i8 {\n return Intrinsic.i64ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: i64) -> Option {\n return Intrinsic.i64CheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: i64) -> i16 {\n return Intrinsic.i64ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: i64) -> Option {\n return Intrinsic.i64CheckedToI16(value)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: i64) -> i32 {\n return Intrinsic.i64ToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: i64) -> Option {\n return Intrinsic.i64CheckedToI32(value)\n}\n\n/// Returns `value` unchanged as `i64`. Use this function when generic conversion code\n/// can select `i64` as both source and destination.\npub fn toI64(value: i64) -> i64 {\n return Intrinsic.i64ToI64(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i64`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI64(value: i64) -> Option {\n return Intrinsic.i64CheckedToI64(value)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: i64) -> isize {\n return Intrinsic.i64ToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: i64) -> Option {\n return Intrinsic.i64CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i64) -> f32 {\n return Intrinsic.i64ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i64) -> f64 {\n return Intrinsic.i64ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i64` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i64` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i64` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i64, right: i64) -> i64 {\n return Intrinsic.i64BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i64, right: i64) -> i64 {\n return Intrinsic.i64BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i64, right: i64) -> i64 {\n return Intrinsic.i64BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i64) -> i64 {\n return Intrinsic.i64BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i64, right: i64) -> i64 {\n return Intrinsic.i64ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i64, right: i64) -> i64 {\n return Intrinsic.i64ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i64, right: i64) -> i64 {\n return Intrinsic.i64RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i64, right: i64) -> i64 {\n return Intrinsic.i64RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i64, right: i64) -> i64 {\n return Intrinsic.i64WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i64, right: i64) -> i64 {\n return Intrinsic.i64WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i64, right: i64) -> i64 {\n return Intrinsic.i64WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i64, right: i64) -> i64 {\n return Intrinsic.i64SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i64, right: i64) -> i64 {\n return Intrinsic.i64SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i64, right: i64) -> i64 {\n return Intrinsic.i64SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i64` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i64` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i64` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i64, right: i64) -> bool {\n return Intrinsic.i64Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i64, right: i64) -> bool {\n return Intrinsic.i64NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i64, right: i64) -> bool {\n return Intrinsic.i64LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i64, right: i64) -> bool {\n return Intrinsic.i64LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i64, right: i64) -> bool {\n return Intrinsic.i64GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i64, right: i64) -> bool {\n return Intrinsic.i64GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i64) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i64`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i64` range.\npub fn parse(text: string) -> Result {\n return Format.i64Value(text)\n}\n', + '//! Sixty-four-bit signed integers for large fixed-width values and exact integer protocols.\n//!\n//! # When to use\n//! Use `i64` when a stable signed 64-bit range is part of storage, interchange, or arithmetic.\n//! Prefer `isize` only for target-sized offsets; its width changes with the compilation target.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`] instead, `wrapping*` uses arithmetic modulo 2^64, and `saturating*`\n//! clamps at [`MIN`] or [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] consumes the whole input and reports syntax separately from range overflow.\n//! [`toText`] allocates owned text. Conversion to `f32` or `f64` can round large exact integers.\n//!\n//! # Gotchas\n//! [`MIN`] has no positive i64 counterpart. Use [`wrappingNegate`] or [`saturatingNegate`] for its\n//! negation boundary. Use [`checkedDivide`] when division by `-1` can receive [`MIN`].\n//!\n//! # Examples\n//! ## Parse a complete signed decimal value\n//! ```silk\n//! import silk.format as Format\n//!\n//! import silk.i64 as i64\n//!\n//! import silk.result as Result\n//!\n//! pub fn main() -> i32 {\n//! let parsed = i64.parse("-42")\n//! let value = move parsed\n//! |> Result.unwrapOr(0)\n//! return i64.toI32(value + 84)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i64` value.\npub const MAX: i64 = 9223372036854775807\n\n/// The smallest `i64` value.\npub const MIN: i64 = -9223372036854775808\n\n/// The fixed width of `i64`, in bits.\npub const BITS: u32 = 64\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i64) -> i64 {\n return Intrinsic.i64Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i64` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i64) -> i64 {\n return Intrinsic.i64WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i64` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i64) -> i64 {\n return Intrinsic.i64SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i64) -> u8 {\n return Intrinsic.i64ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i64) -> Option {\n return Intrinsic.i64CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i64) -> u16 {\n return Intrinsic.i64ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i64) -> Option {\n return Intrinsic.i64CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i64) -> u32 {\n return Intrinsic.i64ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i64) -> Option {\n return Intrinsic.i64CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i64) -> u64 {\n return Intrinsic.i64ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i64) -> Option {\n return Intrinsic.i64CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i64) -> usize {\n return Intrinsic.i64ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i64) -> Option {\n return Intrinsic.i64CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: i64) -> i8 {\n return Intrinsic.i64ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: i64) -> Option {\n return Intrinsic.i64CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: i64) -> i16 {\n return Intrinsic.i64ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: i64) -> Option {\n return Intrinsic.i64CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: i64) -> i32 {\n return Intrinsic.i64ToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: i64) -> Option {\n return Intrinsic.i64CheckedToI32>(value, some, none)\n}\n\n/// Returns `value` unchanged as `i64`. Use this function when generic conversion code\n/// can select `i64` as both source and destination.\npub fn toI64(value: i64) -> i64 {\n return Intrinsic.i64ToI64(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i64`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI64(value: i64) -> Option {\n return Intrinsic.i64CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: i64) -> isize {\n return Intrinsic.i64ToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: i64) -> Option {\n return Intrinsic.i64CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i64) -> f32 {\n return Intrinsic.i64ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i64) -> f64 {\n return Intrinsic.i64ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i64` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i64` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i64` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i64, right: i64) -> i64 {\n return Intrinsic.i64Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i64, right: i64) -> i64 {\n return Intrinsic.i64BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i64, right: i64) -> i64 {\n return Intrinsic.i64BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i64, right: i64) -> i64 {\n return Intrinsic.i64BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i64) -> i64 {\n return Intrinsic.i64BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i64, right: i64) -> i64 {\n return Intrinsic.i64ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i64, right: i64) -> i64 {\n return Intrinsic.i64ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i64, right: i64) -> i64 {\n return Intrinsic.i64RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i64, right: i64) -> i64 {\n return Intrinsic.i64RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i64, right: i64) -> i64 {\n return Intrinsic.i64WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i64, right: i64) -> i64 {\n return Intrinsic.i64WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i64, right: i64) -> i64 {\n return Intrinsic.i64WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i64, right: i64) -> i64 {\n return Intrinsic.i64SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i64, right: i64) -> i64 {\n return Intrinsic.i64SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i64, right: i64) -> i64 {\n return Intrinsic.i64SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i64` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i64` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i64` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i64, right: i64) -> Option {\n return Intrinsic.i64CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i64, right: i64) -> bool {\n return Intrinsic.i64Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i64, right: i64) -> bool {\n return Intrinsic.i64NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i64, right: i64) -> bool {\n return Intrinsic.i64LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i64, right: i64) -> bool {\n return Intrinsic.i64LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i64, right: i64) -> bool {\n return Intrinsic.i64GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i64, right: i64) -> bool {\n return Intrinsic.i64GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i64) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i64`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i64` range.\npub fn parse(text: string) -> Result {\n return Format.i64Value(text)\n}\n', }, { module: 'silk/i8', path: 'silk/i8.silk', sourceIdentity: 'silk/i8', - digest: '4a27f5dbb498964fcec05772b182bd8e7fa51440764f17a743925b9e8d2fe01d', + digest: 'bd34e986eba292606c298ed60fb0555247ee44eaf7cdbe0f4130ee19cf268940', documentation: 'silk/i8.silk', layer: 'portable', runtimeInventory: [ @@ -607,7 +607,7 @@ export const modules = [ ], namespace: 'i8', source: - '//! Eight-bit signed integers for compact values, byte-level formats, and narrow arithmetic.\n//!\n//! # When to use\n//! Use `i8` when an external format or data structure requires exactly eight signed bits. Prefer a\n//! wider integer for general arithmetic: the small range makes accidental overflow easy.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! Choose `checked*` when failure is data, `wrapping*` for arithmetic modulo 2^8, or `saturating*`\n//! when results should clamp to [`MIN`] or [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] consumes the complete input and distinguishes malformed text from a value\n//! outside the i8 range. [`toText`] allocates an owned decimal string.\n//!\n//! # Gotchas\n//! [`MIN`] has no positive i8 counterpart, so ordinary [`negate`] traps for that one value.\n//!\n//! # Examples\n//! ## Select the boundary policy for negation\n//! ```silk\n//! import silk.i8 as i8\n//!\n//! pub fn main() -> i32 {\n//! if i8.wrappingNegate(i8.MIN) != i8.MIN {\n//! return 1\n//! }\n//! if i8.saturatingNegate(i8.MIN) != i8.MAX {\n//! return 2\n//! }\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i8` value.\npub const MAX: i8 = 127\n\n/// The smallest `i8` value.\npub const MIN: i8 = -128\n\n/// The fixed width of `i8`, in bits.\npub const BITS: u32 = 8\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i8) -> i8 {\n return Intrinsic.i8Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i8` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i8) -> i8 {\n return Intrinsic.i8WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i8` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i8) -> i8 {\n return Intrinsic.i8SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i8) -> u8 {\n return Intrinsic.i8ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i8) -> Option {\n return Intrinsic.i8CheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i8) -> u16 {\n return Intrinsic.i8ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i8) -> Option {\n return Intrinsic.i8CheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i8) -> u32 {\n return Intrinsic.i8ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i8) -> Option {\n return Intrinsic.i8CheckedToU32(value)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i8) -> u64 {\n return Intrinsic.i8ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i8) -> Option {\n return Intrinsic.i8CheckedToU64(value)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i8) -> usize {\n return Intrinsic.i8ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i8) -> Option {\n return Intrinsic.i8CheckedToUsize(value)\n}\n\n/// Returns `value` unchanged as `i8`. Use this function when generic conversion code\n/// can select `i8` as both source and destination.\npub fn toI8(value: i8) -> i8 {\n return Intrinsic.i8ToI8(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i8`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI8(value: i8) -> Option {\n return Intrinsic.i8CheckedToI8(value)\n}\n\n/// Converts `value` exactly to `i16`. Every `i8` value is representable.\npub fn toI16(value: i8) -> i16 {\n return Intrinsic.i8ToI16(value)\n}\n\n/// Converts `value` exactly to `i16` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToI16(value: i8) -> Option {\n return Intrinsic.i8CheckedToI16(value)\n}\n\n/// Converts `value` exactly to `i32`. Every `i8` value is representable.\npub fn toI32(value: i8) -> i32 {\n return Intrinsic.i8ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToI32(value: i8) -> Option {\n return Intrinsic.i8CheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `i8` value is representable.\npub fn toI64(value: i8) -> i64 {\n return Intrinsic.i8ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToI64(value: i8) -> Option {\n return Intrinsic.i8CheckedToI64(value)\n}\n\n/// Converts `value` exactly to `isize`. Every `i8` value is representable.\npub fn toIsize(value: i8) -> isize {\n return Intrinsic.i8ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToIsize(value: i8) -> Option {\n return Intrinsic.i8CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i8) -> f32 {\n return Intrinsic.i8ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i8) -> f64 {\n return Intrinsic.i8ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i8` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i8` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i8` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i8, right: i8) -> i8 {\n return Intrinsic.i8BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i8, right: i8) -> i8 {\n return Intrinsic.i8BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i8, right: i8) -> i8 {\n return Intrinsic.i8BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i8) -> i8 {\n return Intrinsic.i8BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i8, right: i8) -> i8 {\n return Intrinsic.i8ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i8, right: i8) -> i8 {\n return Intrinsic.i8ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i8, right: i8) -> i8 {\n return Intrinsic.i8RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i8, right: i8) -> i8 {\n return Intrinsic.i8RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i8, right: i8) -> i8 {\n return Intrinsic.i8WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i8, right: i8) -> i8 {\n return Intrinsic.i8WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i8, right: i8) -> i8 {\n return Intrinsic.i8WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i8, right: i8) -> i8 {\n return Intrinsic.i8SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i8, right: i8) -> i8 {\n return Intrinsic.i8SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i8, right: i8) -> i8 {\n return Intrinsic.i8SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i8` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i8` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i8` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i8, right: i8) -> bool {\n return Intrinsic.i8Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i8, right: i8) -> bool {\n return Intrinsic.i8NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i8, right: i8) -> bool {\n return Intrinsic.i8LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i8, right: i8) -> bool {\n return Intrinsic.i8LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i8, right: i8) -> bool {\n return Intrinsic.i8GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i8, right: i8) -> bool {\n return Intrinsic.i8GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i8) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i8`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i8` range.\npub fn parse(text: string) -> Result {\n return Format.i8Value(text)\n}\n', + '//! Eight-bit signed integers for compact values, byte-level formats, and narrow arithmetic.\n//!\n//! # When to use\n//! Use `i8` when an external format or data structure requires exactly eight signed bits. Prefer a\n//! wider integer for general arithmetic: the small range makes accidental overflow easy.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! Choose `checked*` when failure is data, `wrapping*` for arithmetic modulo 2^8, or `saturating*`\n//! when results should clamp to [`MIN`] or [`MAX`]. Signed right shifts preserve the sign bit.\n//!\n//! Decimal [`parse`] consumes the complete input and distinguishes malformed text from a value\n//! outside the i8 range. [`toText`] allocates an owned decimal string.\n//!\n//! # Gotchas\n//! [`MIN`] has no positive i8 counterpart, so ordinary [`negate`] traps for that one value.\n//!\n//! # Examples\n//! ## Select the boundary policy for negation\n//! ```silk\n//! import silk.i8 as i8\n//!\n//! pub fn main() -> i32 {\n//! if i8.wrappingNegate(i8.MIN) != i8.MIN {\n//! return 1\n//! }\n//! if i8.saturatingNegate(i8.MIN) != i8.MAX {\n//! return 2\n//! }\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `i8` value.\npub const MAX: i8 = 127\n\n/// The smallest `i8` value.\npub const MIN: i8 = -128\n\n/// The fixed width of `i8`, in bits.\npub const BITS: u32 = 8\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: i8) -> i8 {\n return Intrinsic.i8Negate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `i8` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: i8) -> i8 {\n return Intrinsic.i8WrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `i8` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: i8) -> i8 {\n return Intrinsic.i8SaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: i8) -> u8 {\n return Intrinsic.i8ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: i8) -> Option {\n return Intrinsic.i8CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: i8) -> u16 {\n return Intrinsic.i8ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: i8) -> Option {\n return Intrinsic.i8CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: i8) -> u32 {\n return Intrinsic.i8ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: i8) -> Option {\n return Intrinsic.i8CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: i8) -> u64 {\n return Intrinsic.i8ToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: i8) -> Option {\n return Intrinsic.i8CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: i8) -> usize {\n return Intrinsic.i8ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: i8) -> Option {\n return Intrinsic.i8CheckedToUsize>(value, some, none)\n}\n\n/// Returns `value` unchanged as `i8`. Use this function when generic conversion code\n/// can select `i8` as both source and destination.\npub fn toI8(value: i8) -> i8 {\n return Intrinsic.i8ToI8(value)\n}\n\n/// Returns `Some` with `value` unchanged as `i8`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToI8(value: i8) -> Option {\n return Intrinsic.i8CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` exactly to `i16`. Every `i8` value is representable.\npub fn toI16(value: i8) -> i16 {\n return Intrinsic.i8ToI16(value)\n}\n\n/// Converts `value` exactly to `i16` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToI16(value: i8) -> Option {\n return Intrinsic.i8CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` exactly to `i32`. Every `i8` value is representable.\npub fn toI32(value: i8) -> i32 {\n return Intrinsic.i8ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToI32(value: i8) -> Option {\n return Intrinsic.i8CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `i8` value is representable.\npub fn toI64(value: i8) -> i64 {\n return Intrinsic.i8ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToI64(value: i8) -> Option {\n return Intrinsic.i8CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` exactly to `isize`. Every `i8` value is representable.\npub fn toIsize(value: i8) -> isize {\n return Intrinsic.i8ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `i8` value is\n/// representable.\npub fn checkedToIsize(value: i8) -> Option {\n return Intrinsic.i8CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: i8) -> f32 {\n return Intrinsic.i8ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: i8) -> f64 {\n return Intrinsic.i8ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `i8` range. Use this function\n/// when overflow is a program error.\npub fn add(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `i8` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `i8` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Multiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Divide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: i8, right: i8) -> i8 {\n return Intrinsic.i8Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: i8, right: i8) -> i8 {\n return Intrinsic.i8BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: i8, right: i8) -> i8 {\n return Intrinsic.i8BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: i8, right: i8) -> i8 {\n return Intrinsic.i8BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: i8) -> i8 {\n return Intrinsic.i8BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: i8, right: i8) -> i8 {\n return Intrinsic.i8ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: i8, right: i8) -> i8 {\n return Intrinsic.i8ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: i8, right: i8) -> i8 {\n return Intrinsic.i8RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: i8, right: i8) -> i8 {\n return Intrinsic.i8RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `i8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: i8, right: i8) -> i8 {\n return Intrinsic.i8WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `i8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: i8, right: i8) -> i8 {\n return Intrinsic.i8WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `i8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: i8, right: i8) -> i8 {\n return Intrinsic.i8WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: i8, right: i8) -> i8 {\n return Intrinsic.i8SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: i8, right: i8) -> i8 {\n return Intrinsic.i8SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: i8, right: i8) -> i8 {\n return Intrinsic.i8SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `i8` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `i8` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `i8` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: i8, right: i8) -> Option {\n return Intrinsic.i8CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: i8, right: i8) -> bool {\n return Intrinsic.i8Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: i8, right: i8) -> bool {\n return Intrinsic.i8NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: i8, right: i8) -> bool {\n return Intrinsic.i8LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: i8, right: i8) -> bool {\n return Intrinsic.i8LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: i8, right: i8) -> bool {\n return Intrinsic.i8GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: i8, right: i8) -> bool {\n return Intrinsic.i8GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: i8) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `i8`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` if the number is outside the `i8` range.\npub fn parse(text: string) -> Result {\n return Format.i8Value(text)\n}\n', }, { module: 'silk/insecure_random', @@ -639,7 +639,7 @@ export const modules = [ module: 'silk/isize', path: 'silk/isize.silk', sourceIdentity: 'silk/isize', - digest: 'bf9c2c189f4bc3d41735fd2c8104e1a4d4ecb3af7b4079af8d1114cd84b4677e', + digest: '993d07b6a42a300d12e66b9ea606d8592ae60ba20796ecd4524734d7bbcffd25', documentation: 'silk/isize.silk', layer: 'portable', runtimeInventory: [ @@ -701,46 +701,46 @@ export const modules = [ ], namespace: 'isize', source: - "//! Pointer-width signed integers for offsets whose range follows the selected compilation target.\n//!\n//! # When to use\n//! Use `isize` for offsets paired with target-sized counts or addresses. Use a fixed-width integer\n//! for files, protocols, persistent data, or any value that must mean the same thing on 32-bit and\n//! 64-bit targets.\n//!\n//! # Details\n//! [`BITS`], [`MIN`], and [`MAX`] are selected from the target. Ordinary arithmetic, narrowing\n//! conversions, division by zero, and invalid shift counts trap. `checked*` returns [`Option`],\n//! `wrapping*` uses arithmetic modulo the target width, and `saturating*` clamps at the target bound.\n//!\n//! Decimal [`parse`] and [`toText`] use the selected target range; formatting allocates owned text.\n//!\n//! # Gotchas\n//! Code that succeeds at a 64-bit boundary may fail or trap when compiled for a 32-bit target.\n//! [`MIN`] also has no positive counterpart. Use [`checkedDivide`] when division by `-1` can receive\n//! that value.\n//!\n//! # Examples\n//! ## Clamp an offset at the target boundary\n//! ```silk\n//! import silk.isize as isize\n//!\n//! pub fn main() -> i32 {\n//! if isize.saturatingAdd(isize.MAX, 1) != isize.MAX {\n//! return 1\n//! }\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `isize` value for the compilation target.\n///\n/// # Details\n///\n/// This is 2147483647 on a 32-bit target and 9223372036854775807 on a 64-bit target. Checked\n/// arithmetic rejects results above it.\npub const MAX: isize = Target.isizeMax\n\n/// The smallest `isize` value for the compilation target.\n///\n/// # Details\n///\n/// This is -2147483648 on a 32-bit target and -9223372036854775808 on a 64-bit target.\npub const MIN: isize = Target.isizeMin\n\n/// The width of `isize` in bits, which is the compilation target's pointer width.\npub const BITS: u32 = Target.pointerBits\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: isize) -> isize {\n return Intrinsic.isizeNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `isize` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: isize) -> isize {\n return Intrinsic.isizeWrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `isize` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: isize) -> isize {\n return Intrinsic.isizeSaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: isize) -> u8 {\n return Intrinsic.isizeToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: isize) -> u16 {\n return Intrinsic.isizeToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: isize) -> u32 {\n return Intrinsic.isizeToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU32(value)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: isize) -> u64 {\n return Intrinsic.isizeToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU64(value)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: isize) -> usize {\n return Intrinsic.isizeToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: isize) -> Option {\n return Intrinsic.isizeCheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: isize) -> i8 {\n return Intrinsic.isizeToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: isize) -> i16 {\n return Intrinsic.isizeToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI16(value)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: isize) -> i32 {\n return Intrinsic.isizeToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `isize` value is representable.\npub fn toI64(value: isize) -> i64 {\n return Intrinsic.isizeToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `isize` value is\n/// representable.\npub fn checkedToI64(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI64(value)\n}\n\n/// Returns `value` unchanged as `isize`. Use this function when generic conversion code\n/// can select `isize` as both source and destination.\npub fn toIsize(value: isize) -> isize {\n return Intrinsic.isizeToIsize(value)\n}\n\n/// Returns `Some` with `value` unchanged as `isize`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToIsize(value: isize) -> Option {\n return Intrinsic.isizeCheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: isize) -> f32 {\n return Intrinsic.isizeToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: isize) -> f64 {\n return Intrinsic.isizeToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `isize` range. Use this function\n/// when overflow is a program error.\npub fn add(left: isize, right: isize) -> isize {\n return Intrinsic.isizeAdd(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `isize` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSubtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `isize` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: isize, right: isize) -> isize {\n return Intrinsic.isizeMultiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: isize, right: isize) -> isize {\n return Intrinsic.isizeDivide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: isize, right: isize) -> isize {\n return Intrinsic.isizeRemainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: isize, right: isize) -> isize {\n return Intrinsic.isizeBitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: isize, right: isize) -> isize {\n return Intrinsic.isizeBitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: isize, right: isize) -> isize {\n return Intrinsic.isizeBitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: isize) -> isize {\n return Intrinsic.isizeBitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: isize, right: isize) -> isize {\n return Intrinsic.isizeShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: isize, right: isize) -> isize {\n return Intrinsic.isizeShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: isize, right: isize) -> isize {\n return Intrinsic.isizeRotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: isize, right: isize) -> isize {\n return Intrinsic.isizeRotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `isize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: isize, right: isize) -> isize {\n return Intrinsic.isizeWrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `isize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: isize, right: isize) -> isize {\n return Intrinsic.isizeWrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `isize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: isize, right: isize) -> isize {\n return Intrinsic.isizeWrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `isize` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `isize` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `isize` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: isize, right: isize) -> bool {\n return Intrinsic.isizeEquals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: isize, right: isize) -> bool {\n return Intrinsic.isizeNotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: isize, right: isize) -> bool {\n return Intrinsic.isizeLessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: isize, right: isize) -> bool {\n return Intrinsic.isizeLessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: isize, right: isize) -> bool {\n return Intrinsic.isizeGreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: isize, right: isize) -> bool {\n return Intrinsic.isizeGreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: isize) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `isize`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` outside the target's `isize` range.\npub fn parse(text: string) -> Result {\n return Format.isizeValue(text)\n}\n", + "//! Pointer-width signed integers for offsets whose range follows the selected compilation target.\n//!\n//! # When to use\n//! Use `isize` for offsets paired with target-sized counts or addresses. Use a fixed-width integer\n//! for files, protocols, persistent data, or any value that must mean the same thing on 32-bit and\n//! 64-bit targets.\n//!\n//! # Details\n//! [`BITS`], [`MIN`], and [`MAX`] are selected from the target. Ordinary arithmetic, narrowing\n//! conversions, division by zero, and invalid shift counts trap. `checked*` returns [`Option`],\n//! `wrapping*` uses arithmetic modulo the target width, and `saturating*` clamps at the target bound.\n//!\n//! Decimal [`parse`] and [`toText`] use the selected target range; formatting allocates owned text.\n//!\n//! # Gotchas\n//! Code that succeeds at a 64-bit boundary may fail or trap when compiled for a 32-bit target.\n//! [`MIN`] also has no positive counterpart. Use [`checkedDivide`] when division by `-1` can receive\n//! that value.\n//!\n//! # Examples\n//! ## Clamp an offset at the target boundary\n//! ```silk\n//! import silk.isize as isize\n//!\n//! pub fn main() -> i32 {\n//! if isize.saturatingAdd(isize.MAX, 1) != isize.MAX {\n//! return 1\n//! }\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `isize` value for the compilation target.\n///\n/// # Details\n///\n/// This is 2147483647 on a 32-bit target and 9223372036854775807 on a 64-bit target. Checked\n/// arithmetic rejects results above it.\npub const MAX: isize = Target.isizeMax\n\n/// The smallest `isize` value for the compilation target.\n///\n/// # Details\n///\n/// This is -2147483648 on a 32-bit target and -9223372036854775808 on a 64-bit target.\npub const MIN: isize = Target.isizeMin\n\n/// The width of `isize` in bits, which is the compilation target's pointer width.\npub const BITS: u32 = Target.pointerBits\n\n/// Returns the arithmetic negation of `value` and traps when `value` is [`MIN`]. Use\n/// this function when that boundary is a program error.\npub fn negate(value: isize) -> isize {\n return Intrinsic.isizeNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, wrapped to the `isize` range. [`MIN`]\n/// stays [`MIN`]. Use this function for deliberate modulo arithmetic.\npub fn wrappingNegate(value: isize) -> isize {\n return Intrinsic.isizeWrappingNegate(value)\n}\n\n/// Returns the arithmetic negation of `value`, clamped to the `isize` range. [`MIN`]\n/// becomes [`MAX`]. Use this function when the positive boundary is required.\npub fn saturatingNegate(value: isize) -> isize {\n return Intrinsic.isizeSaturatingNegate(value)\n}\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: isize) -> u8 {\n return Intrinsic.isizeToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: isize) -> u16 {\n return Intrinsic.isizeToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: isize) -> u32 {\n return Intrinsic.isizeToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU32>(value, some, none)\n}\n\n/// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU64(value: isize) -> u64 {\n return Intrinsic.isizeToU64(value)\n}\n\n/// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU64(value: isize) -> Option {\n return Intrinsic.isizeCheckedToU64>(value, some, none)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: isize) -> usize {\n return Intrinsic.isizeToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: isize) -> Option {\n return Intrinsic.isizeCheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: isize) -> i8 {\n return Intrinsic.isizeToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: isize) -> i16 {\n return Intrinsic.isizeToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI16>(value, some, none)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: isize) -> i32 {\n return Intrinsic.isizeToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `isize` value is representable.\npub fn toI64(value: isize) -> i64 {\n return Intrinsic.isizeToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `isize` value is\n/// representable.\npub fn checkedToI64(value: isize) -> Option {\n return Intrinsic.isizeCheckedToI64>(value, some, none)\n}\n\n/// Returns `value` unchanged as `isize`. Use this function when generic conversion code\n/// can select `isize` as both source and destination.\npub fn toIsize(value: isize) -> isize {\n return Intrinsic.isizeToIsize(value)\n}\n\n/// Returns `Some` with `value` unchanged as `isize`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToIsize(value: isize) -> Option {\n return Intrinsic.isizeCheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: isize) -> f32 {\n return Intrinsic.isizeToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: isize) -> f64 {\n return Intrinsic.isizeToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `isize` range. Use this function\n/// when overflow is a program error.\npub fn add(left: isize, right: isize) -> isize {\n return Intrinsic.isizeAdd(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `isize` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSubtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `isize` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: isize, right: isize) -> isize {\n return Intrinsic.isizeMultiply(left, right)\n}\n\n/// Returns `left / right`, rounded toward zero. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is a program error.\npub fn divide(left: isize, right: isize) -> isize {\n return Intrinsic.isizeDivide(left, right)\n}\n\n/// Returns the remainder with the sign of `left`. Traps if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is a program error.\npub fn remainder(left: isize, right: isize) -> isize {\n return Intrinsic.isizeRemainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: isize, right: isize) -> isize {\n return Intrinsic.isizeBitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: isize, right: isize) -> isize {\n return Intrinsic.isizeBitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: isize, right: isize) -> isize {\n return Intrinsic.isizeBitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: isize) -> isize {\n return Intrinsic.isizeBitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is negative or not less than\n/// [`BITS`].\npub fn shiftLeft(left: isize, right: isize) -> isize {\n return Intrinsic.isizeShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and preserves its sign. Traps if `right` is\n/// negative or not less than [`BITS`].\npub fn shiftRight(left: isize, right: isize) -> isize {\n return Intrinsic.isizeShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: isize, right: isize) -> isize {\n return Intrinsic.isizeRotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: isize, right: isize) -> isize {\n return Intrinsic.isizeRotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `isize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: isize, right: isize) -> isize {\n return Intrinsic.isizeWrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `isize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: isize, right: isize) -> isize {\n return Intrinsic.isizeWrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `isize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: isize, right: isize) -> isize {\n return Intrinsic.isizeWrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: isize, right: isize) -> isize {\n return Intrinsic.isizeSaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `isize` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `isize` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `isize` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when an invalid quotient is input data.\npub fn checkedDivide(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is\n/// divided by `-1`. Use this function when invalid division is input data.\npub fn checkedRemainder(left: isize, right: isize) -> Option {\n return Intrinsic.isizeCheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: isize, right: isize) -> bool {\n return Intrinsic.isizeEquals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: isize, right: isize) -> bool {\n return Intrinsic.isizeNotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: isize, right: isize) -> bool {\n return Intrinsic.isizeLessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: isize, right: isize) -> bool {\n return Intrinsic.isizeLessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: isize, right: isize) -> bool {\n return Intrinsic.isizeGreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: isize, right: isize) -> bool {\n return Intrinsic.isizeGreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: isize) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.signedText(toI64(value))\n}\n\n/// Reads the complete text as a signed decimal `isize`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a leading `+`, a non-digit, or\n/// trailing bytes. It contains `silk.format.OutOfRange` outside the target's `isize` range.\npub fn parse(text: string) -> Result {\n return Format.isizeValue(text)\n}\n", }, { module: 'silk/layout', path: 'silk/layout.silk', sourceIdentity: 'silk/layout', - digest: '26fb2843b26c47f982efff7d4103620e49bf4501bd10f53bd1eb5d96794b8114', + digest: '361afaaad771349a629a491b1514398888e3a87c3161144308f43bdf68aff5c3', documentation: 'silk/layout.silk', layer: 'portable', runtimeInventory: ['layoutOf'], namespace: 'Layout', aliases: ['InvalidAlignment', 'LayoutOverflow'], source: - '//! Checked size-and-alignment descriptions used to request storage from an allocator.\n//!\n//! # When to use\n//! Prefer [`of`] when storage will hold a known Silk type. Use [`make`] only at an untyped boundary\n//! that already knows a byte size and alignment, and [`repeat`] when sizing contiguous elements.\n//!\n//! # Details\n//! Alignments are non-zero powers of two. [`repeat`] preserves the element alignment and reports\n//! [`LayoutOverflow`] instead of wrapping the aggregate byte size. A [`Layout`] describes storage;\n//! it does not allocate or initialize it.\n//!\n//! # Examples\n//! ## Size storage for repeated values\n//! ```silk\n//! import silk.layout as Layout\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let element = Layout.of()\n//! return match move Layout.repeat(move element, 3) {\n//! Layout.Layout {bytes, alignment} => usize.toI32(bytes) + 30\n//! Layout.LayoutOverflow {} => 0\n//! }\n//! }\n//! ```\n\nimport silk.option { None }\nimport silk.option { Some }\nimport silk.usize as usize\n\n/// A byte size paired with a non-zero power-of-two alignment.\npub struct Layout {\n /// The number of addressable bytes required by the described storage.\n pub bytes: usize\n /// The required non-zero power-of-two byte alignment.\n pub alignment: usize\n}\n\n/// Reports the rejected alignment supplied to [`make`].\npub struct InvalidAlignment {\n /// The zero or non-power-of-two alignment that was rejected.\n pub alignment: usize\n}\n\n/// Reports that [`repeat`] could not represent the aggregate byte size as `usize`.\npub struct LayoutOverflow {}\n\n/// Returns the byte size and alignment required to store one value of `T`.\npub fn of() -> Layout {\n return Intrinsic.layoutOf()\n}\n\n/// Creates a layout from an explicit byte size and alignment.\n///\n/// # Gotchas\n///\n/// If `alignment` is zero or is not a power of two, returns `InvalidAlignment` with that value.\npub fn make(size: usize, alignment: usize) -> Layout | InvalidAlignment {\n if alignment == 0 {\n return InvalidAlignment { alignment: alignment }\n }\n let previous = alignment - 1\n if usize.bitAnd(alignment, previous) != 0 {\n return InvalidAlignment { alignment: alignment }\n }\n return Layout { bytes: size, alignment: alignment }\n}\n\n/// Returns a layout for `count` contiguous instances without wrapping the total byte size.\n///\n/// # Details\n///\n/// The result keeps the input alignment and multiplies its byte size by `count`.\n///\n/// # Gotchas\n///\n/// If the total byte size does not fit in `usize`, returns `LayoutOverflow`.\npub fn repeat(layout: Layout, count: usize) -> Layout | LayoutOverflow {\n return match move usize.checkedMultiply(layout.bytes, count) {\n None {} => LayoutOverflow {}\n Some { value } => Layout { bytes: value, alignment: layout.alignment }\n }\n}\n', + '//! Checked size-and-alignment descriptions used to request storage from an allocator.\n//!\n//! # When to use\n//! Prefer [`of`] when storage will hold a known Silk type. Use [`make`] only at an untyped boundary\n//! that already knows a byte size and alignment, and [`repeat`] when sizing contiguous elements.\n//!\n//! # Details\n//! Alignments are non-zero powers of two. [`repeat`] preserves the element alignment and reports\n//! [`LayoutOverflow`] instead of wrapping the aggregate byte size. A [`Layout`] describes storage;\n//! it does not allocate or initialize it.\n//!\n//! # Examples\n//! ## Size storage for repeated values\n//! ```silk\n//! import silk.layout as Layout\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let element = Layout.of()\n//! return match move Layout.repeat(move element, 3) {\n//! Layout.Layout {bytes, alignment} => usize.toI32(bytes) + 30\n//! Layout.LayoutOverflow {} => 0\n//! }\n//! }\n//! ```\n\nimport silk.option { Option }\nimport silk.usize as usize\n\n/// A byte size paired with a non-zero power-of-two alignment.\npub struct Layout {\n /// The number of addressable bytes required by the described storage.\n pub bytes: usize\n /// The required non-zero power-of-two byte alignment.\n pub alignment: usize\n}\n\n/// Reports the rejected alignment supplied to [`make`].\npub struct InvalidAlignment {\n /// The zero or non-power-of-two alignment that was rejected.\n pub alignment: usize\n}\n\n/// Reports that [`repeat`] could not represent the aggregate byte size as `usize`.\npub struct LayoutOverflow {}\n\n/// Returns the byte size and alignment required to store one value of `T`.\npub fn of() -> Layout {\n return Intrinsic.layoutOf()\n}\n\n/// Creates a layout from an explicit byte size and alignment.\n///\n/// # Gotchas\n///\n/// If `alignment` is zero or is not a power of two, returns `InvalidAlignment` with that value.\npub fn make(size: usize, alignment: usize) -> Layout | InvalidAlignment {\n if alignment == 0 {\n return InvalidAlignment { alignment: alignment }\n }\n let previous = alignment - 1\n if usize.bitAnd(alignment, previous) != 0 {\n return InvalidAlignment { alignment: alignment }\n }\n return Layout { bytes: size, alignment: alignment }\n}\n\n/// Returns a layout for `count` contiguous instances without wrapping the total byte size.\n///\n/// # Details\n///\n/// The result keeps the input alignment and multiplies its byte size by `count`.\n///\n/// # Gotchas\n///\n/// If the total byte size does not fit in `usize`, returns `LayoutOverflow`.\npub fn repeat(layout: Layout, count: usize) -> Layout | LayoutOverflow {\n return match move usize.checkedMultiply(layout.bytes, count) {\n Option.None => LayoutOverflow {}\n Option.Some { value } => Layout { bytes: value, alignment: layout.alignment }\n }\n}\n', }, { module: 'silk/local_scheduler', path: 'silk/local_scheduler.silk', sourceIdentity: 'silk/local_scheduler', - digest: 'facf7a0faadf97857ddce79e120885eaf40232a15c29dc4284a52bd215756126', + digest: 'd38152f805247270fcf758f4fb4b69991d1d05162ed75559cdccd56ba79e4c3d', documentation: 'silk/local_scheduler.silk', layer: 'portable', runtimeInventory: ['Execution', 'Wake', 'replace', 'wake'], namespace: 'LocalScheduler', aliases: ['StalledError'], source: - '//! Deterministic single-threaded execution for structured Fibers.\n//!\n//! # When to use\n//! Use [`execute`] at an application entry point to run one lazy program that uses the\n//! `silk.scheduler.Scheduler` service.\n//!\n//! # Details\n//! Each call creates fresh task storage and a FIFO ready queue. The root is task zero and uses the\n//! same `Execution<()>` storage as every child. [`execute`] returns only after the root terminates.\n\nimport silk.allocator { Allocator, OutOfMemoryError }\nimport silk.effect as Effect\nimport silk.execution as Execution\nimport silk.fiber as Fiber\nimport silk.hash as Hash\nimport silk.hash_map as HashMap\nimport silk.option { None, Option, Some }\nimport silk.result { Failure, Result, Success }\nimport silk.scheduler as Scheduler\nimport silk.shared as Shared\nimport silk.u64 as u64\n\n/// A reusable deterministic single-threaded Scheduler provider.\n///\n/// # Details\n///\n/// Each call to [`execute`] creates fresh task storage and readiness state.\npub struct LocalScheduler {}\n\n/// Reports that no task is ready while the root is incomplete.\n///\n/// # Details\n///\n/// [`execute`] cancels and releases the incomplete task tree before it raises this error.\npub struct StalledError {}\n\n/// Constructs a reusable local Scheduler value.\npub fn make() -> LocalScheduler {\n return LocalScheduler {}\n}\n\nstruct NoLink {}\nstruct TaskLink { identity: Scheduler.TaskId }\nimpl Copy for NoLink {}\nimpl Copy for TaskLink {}\n\nstruct ReadyNode {\n identity: Scheduler.TaskId\n next: NoLink | ReadyNodeLink\n enqueued: bool\n}\n\nstruct ReadyNodeLink { node: Shared.Shared }\nstruct ReadyQueue {\n head: NoLink | ReadyNodeLink\n tail: NoLink | ReadyNodeLink\n}\n\nstruct ReadyEndpoint {\n queue: Shared.Shared\n node: Shared.Shared\n}\n\nfn claimReady(node: &mut ReadyNode) -> bool {\n if node.enqueued { return false }\n node.enqueued = true\n return true\n}\n\nfn linkReady(node: &mut ReadyNode, next: Shared.Shared) -> () {\n let previous = Intrinsic.replace(node.next, ReadyNodeLink { node: move next })\n drop previous\n return ()\n}\n\nfn installFirst(queue: &mut ReadyQueue, node: Shared.Shared) -> () {\n queue.head = ReadyNodeLink { node: move node }\n return ()\n}\n\nfn installAfter(previous: Shared.Shared, node: Shared.Shared) -> () {\n let installed = Shared.withMut(&previous, linkReady(move node))\n drop previous\n return ()\n}\n\nfn appendReady(queue: &mut ReadyQueue, node: Shared.Shared) -> () {\n let accepted = Shared.withMut(&node, claimReady)\n if !accepted { drop node return () }\n let retained = Shared.clone(&node)\n let previous = Intrinsic.replace(queue.tail, ReadyNodeLink { node: move retained })\n return match move previous {\n NoLink {} => installFirst(move queue, move node)\n ReadyNodeLink { node: tail } => installAfter(move tail, move node)\n }\n}\n\nfn notifyReady(endpoint: &ReadyEndpoint) -> () {\n let queue = Shared.clone(&endpoint.queue)\n let node = Shared.clone(&endpoint.node)\n let appended = Shared.withMut(&queue, appendReady(move node))\n drop queue\n return ()\n}\n\nfn takeNext(node: &mut ReadyNode) -> NoLink | ReadyNodeLink {\n return Intrinsic.replace(node.next, NoLink {})\n}\n\nfn clearTail(queue: &mut ReadyQueue) -> () {\n let previous = Intrinsic.replace(queue.tail, NoLink {})\n drop previous\n return ()\n}\n\nfn advanceHead(queue: &mut ReadyQueue, next: NoLink | ReadyNodeLink) -> () {\n return match move next {\n NoLink {} => clearTail(move queue)\n ReadyNodeLink { node } => installFirst(move queue, move node)\n }\n}\n\nfn takeLinked(\n queue: &mut ReadyQueue,\n node: Shared.Shared,\n) -> NoLink | ReadyNodeLink {\n let next = Shared.withMut(&node, takeNext)\n advanceHead(move queue, move next)\n return ReadyNodeLink { node: move node }\n}\n\nfn takeHead(queue: &mut ReadyQueue) -> NoLink | ReadyNodeLink {\n let selected = Intrinsic.replace(queue.head, NoLink {})\n return match move selected {\n NoLink {} => NoLink {}\n ReadyNodeLink { node } => takeLinked(move queue, move node)\n }\n}\n\nfn releaseReady(node: &mut ReadyNode) -> Scheduler.TaskId {\n node.enqueued = false\n return node.identity\n}\n\nstruct QueueEmpty {}\nstruct ReadyIdentity { identity: Scheduler.TaskId }\n\nfn releaseSelected(node: Shared.Shared) -> ReadyIdentity {\n let identity = Shared.withMut(&node, releaseReady)\n drop node\n return ReadyIdentity { identity: identity }\n}\n\nfn takeReady(queue: &Shared.Shared) -> QueueEmpty | ReadyIdentity {\n let selected = Shared.withMut(queue, takeHead)\n return match move selected {\n NoLink {} => QueueEmpty {}\n ReadyNodeLink { node } => releaseSelected(move node)\n }\n}\n\nstruct TaskIdSource { next: u64 exhausted: bool }\nstruct Reserved { identity: Scheduler.TaskId }\nstruct Refused {}\n\nfn reserveStep(source: &mut TaskIdSource) -> Reserved | Refused {\n if source.exhausted { return Refused {} }\n let selected = source.next\n if selected == u64.MAX { source.exhausted = true } else { source.next = selected + 1 }\n return Reserved { identity: Scheduler.TaskId { value: selected } }\n}\n\neffect fn exhausted() -> Scheduler.TaskId ! Scheduler.TaskIdExhaustedError {\n fail Scheduler.TaskIdExhaustedError {}\n}\n\neffect fn reserve(source: &Shared.Shared) -> Scheduler.TaskId\n! Scheduler.TaskIdExhaustedError {\n let selected = Shared.withMut(source, reserveStep)\n return match move selected {\n Reserved { identity } => move identity\n Refused {} => run exhausted()\n }\n}\n\nstruct SchedulerClient {\n identity: Scheduler.TaskId\n mailbox: Shared.Shared\n submission: Shared.Shared\n response: Shared.Shared\n identities: Shared.Shared\n queue: Shared.Shared\n}\n\nfn publishTaskResult(\n result: Result,\n producer: Fiber.CompletionProducer,\n) -> () {\n let Result { value } = move result\n return match move value {\n Success { value: success } => Fiber.completeSuccess(move producer, move success)\n Failure { error } => Fiber.completeFailure(move producer, move error)\n }\n}\n\neffect fn runTask(\n body: once Effect,\n client: SchedulerClient,\n producer: Fiber.CompletionProducer,\n) -> () {\n let result = run Effect.bindRequirementOwned(\n Effect.result(move body),\n move client,\n )\n publishTaskResult(move result, move producer)\n return ()\n}\n\nstruct RootPending {}\nstruct RootReady { result: Result }\nstruct RootState { phase: RootPending | RootReady }\nstruct RootTaskCompleted {}\nstruct RootTaskNeverFailed {}\n\nfn storeRoot(state: &mut RootState, result: Result) -> () {\n let previous = Intrinsic.replace(state.phase, RootReady { result: move result })\n drop previous\n return ()\n}\n\nfn takeRoot(state: &mut RootState) -> RootPending | RootReady {\n return Intrinsic.replace(state.phase, RootPending {})\n}\n\nstruct RootExtraction { phase: RootPending | RootReady }\n\nfn extractRoot(state: &mut RootState, output: &mut RootExtraction) -> () {\n output.phase = Intrinsic.replace(state.phase, RootPending {})\n return ()\n}\n\neffect fn runRoot(\n body: once Effect,\n client: SchedulerClient,\n state: Shared.Shared>,\n producer: Fiber.CompletionProducer,\n) -> () {\n let result = run Effect.bindRequirementOwned(\n Effect.result(move body),\n move client,\n )\n let stored = Shared.withMut(&state, storeRoot(move result))\n drop state\n Fiber.completeSuccess(\n move producer,\n RootTaskCompleted {},\n )\n return ()\n}\n\neffect fn prepareChild(\n self: &mut SchedulerClient,\n child: once Effect,\n) -> Scheduler.PendingPublication\n! OutOfMemoryError | Scheduler.TaskIdExhaustedError {\n let identity = run reserve(&self.identities)\n let mut allocator = Allocator.systemAllocatorProvider()\n let prepared = run Fiber.prepare() |> Effect.provideMut(&mut allocator)\n let response = run Shared.make(Scheduler.PublicationResponse {\n phase: Scheduler.ResponseWaiting {},\n }) |> Effect.provideMut(&mut allocator)\n let childSubmission = run Shared.make(Scheduler.SubmissionSlot {\n phase: Scheduler.NoSubmission {},\n }) |> Effect.provideMut(&mut allocator)\n let mailboxResponse = Shared.clone(&response)\n let childMailbox = run Shared.make(Scheduler.TaskMailbox {\n request: Scheduler.NoRequest {},\n response: move mailboxResponse,\n }) |> Effect.provideMut(&mut allocator)\n let node = run Shared.make(ReadyNode {\n identity: identity,\n next: NoLink {},\n enqueued: false,\n }) |> Effect.provideMut(&mut allocator)\n let Fiber.PreparedFiber { producer, canceller, fiber } = move prepared\n let client = SchedulerClient {\n identity: identity,\n mailbox: Shared.clone(&childMailbox),\n submission: Shared.clone(&childSubmission),\n response: move response,\n identities: Shared.clone(&self.identities),\n queue: Shared.clone(&self.queue),\n }\n let endpoint = ReadyEndpoint {\n queue: Shared.clone(&self.queue),\n node: move node,\n }\n let execution = run Execution.make(\n runTask(move child, move client, move producer),\n move endpoint,\n notifyReady,\n ) |> Effect.provideMut(&mut allocator)\n let task = Scheduler.PreparedTask {\n identity: identity,\n parent: self.identity,\n execution: move execution,\n canceller: move canceller,\n mailbox: move childMailbox,\n submission: move childSubmission,\n }\n return Scheduler.pending(\n move fiber,\n Shared.clone(&self.mailbox),\n Shared.clone(&self.submission),\n Shared.clone(&self.response),\n move task,\n )\n}\n\nimpl Scheduler.Scheduler for SchedulerClient { prepare: SchedulerClient.prepareChild }\n\nstruct Initial {}\nstruct Running {}\nstruct Dormant {}\nstruct Eligible {}\nstruct Completed {}\nstruct NoExecution {}\nstruct StoredExecution { execution: Intrinsic.Execution<()> }\n\nstruct TaskEntry {\n parent: NoLink | TaskLink\n firstChild: NoLink | TaskLink\n nextSibling: NoLink | TaskLink\n cancellationNext: NoLink | TaskLink\n phase: Initial | Running | Dormant | Eligible | Completed\n execution: NoExecution | StoredExecution\n canceller: Fiber.CompletionCanceller\n mailbox: Shared.Shared\n submission: Shared.Shared\n}\n\nstruct Driver {\n tasks: HashMap.HashMap\n queue: Shared.Shared\n current: Scheduler.TaskId\n cancellationHead: NoLink | TaskLink\n rootComplete: bool\n}\n\nstruct Extraction { slot: NoExecution | StoredExecution }\n\nfn extract(entry: &mut TaskEntry, output: &mut Extraction) -> () {\n output.slot = Intrinsic.replace(entry.execution, NoExecution {})\n entry.phase = Running {}\n return ()\n}\n\nfn markEligible(entry: &mut TaskEntry) -> () {\n entry.phase = Eligible {}\n return ()\n}\n\nfn restore(entry: &mut TaskEntry, execution: Intrinsic.Execution<()>) -> () {\n entry.execution = StoredExecution { execution: move execution }\n entry.phase = Dormant {}\n return ()\n}\n\nfn notifyStoredInitial(\n entry: &mut TaskEntry,\n execution: Intrinsic.Execution<()>,\n) -> () {\n let mut selected = move execution\n Execution.notifyInitial(&mut selected)\n entry.execution = StoredExecution { execution: move selected }\n entry.phase = Eligible {}\n return ()\n}\n\nfn notifyInitialEntry(entry: &mut TaskEntry) -> () {\n let selected = Intrinsic.replace(entry.execution, NoExecution {})\n return match move selected {\n NoExecution {} => missingInitialExecution()\n StoredExecution { execution } => notifyStoredInitial(move entry, move execution)\n }\n}\n\nfn missingInitialExecution() -> () {\n let invalid = 1 / 0\n return missingInitialExecution()\n}\n\nfn markCompleted(entry: &mut TaskEntry, result: ()) -> () {\n drop result\n entry.phase = Completed {}\n return ()\n}\n\nstruct ActivationState {\n slot: NoExecution | StoredExecution\n completed: bool\n}\n\nfn suspended(state: &mut ActivationState, execution: Intrinsic.Execution<()>) -> () {\n state.slot = StoredExecution { execution: move execution }\n return ()\n}\n\nfn completed(state: &mut ActivationState, result: ()) -> () {\n drop result\n state.completed = true\n return ()\n}\n\neffect fn driveSelected(\n selected: NoExecution | StoredExecution,\n state: &mut ActivationState,\n) -> () {\n return match move selected {\n NoExecution {} => ()\n StoredExecution { execution } => run Execution.drive(\n move execution,\n move state,\n completed,\n suspended,\n )\n }\n}\n\nfn takeRequest(\n mailbox: &mut Scheduler.TaskMailbox,\n) -> Scheduler.NoRequest | Scheduler.PendingRequest {\n return Intrinsic.replace(mailbox.request, Scheduler.NoRequest {})\n}\n\nfn takeSubmission(\n slot: &mut Scheduler.SubmissionSlot,\n) -> Scheduler.NoSubmission | Scheduler.PendingSubmission {\n return Intrinsic.replace(slot.phase, Scheduler.NoSubmission {})\n}\n\nstruct NoMailbox {}\nstruct MailboxHandle { value: Shared.Shared }\nstruct NoSubmissionHandle {}\nstruct SubmissionHandle { value: Shared.Shared }\nstruct EntryHandles {\n mailbox: NoMailbox | MailboxHandle\n submission: NoSubmissionHandle | SubmissionHandle\n}\n\nfn copyHandles(entry: &mut TaskEntry, output: &mut EntryHandles) -> () {\n output.mailbox = MailboxHandle {\n value: Shared.clone(&entry.mailbox),\n }\n output.submission = SubmissionHandle {\n value: Shared.clone(&entry.submission),\n }\n return ()\n}\n\nfn markPublished(response: &mut Scheduler.PublicationResponse) -> () {\n response.phase = Scheduler.ResponsePublished {}\n return ()\n}\n\nfn markRejected(response: &mut Scheduler.PublicationResponse) -> () {\n response.phase = Scheduler.ResponseRejected {}\n return ()\n}\n\nfn setFirstChild(entry: &mut TaskEntry, child: Scheduler.TaskId) -> () {\n entry.firstChild = TaskLink { identity: child }\n return ()\n}\n\nfn makeEntry(task: Scheduler.PreparedTask, sibling: NoLink | TaskLink) -> TaskEntry {\n let Scheduler.PreparedTask {\n identity,\n parent,\n execution,\n canceller,\n mailbox,\n submission,\n } = move task\n return TaskEntry {\n parent: TaskLink { identity: parent },\n firstChild: NoLink {},\n nextSibling: move sibling,\n cancellationNext: NoLink {},\n phase: Initial {},\n execution: StoredExecution { execution: move execution },\n canceller: move canceller,\n mailbox: move mailbox,\n submission: move submission,\n }\n}\n\nstruct LinkOutput { link: NoLink | TaskLink }\nfn copyFirstChild(entry: &mut TaskEntry, output: &mut LinkOutput) -> () {\n output.link = entry.firstChild\n return ()\n}\n\neffect fn adopt(\n driver: &mut Driver,\n parent: Scheduler.TaskId,\n wake: Intrinsic.Wake,\n response: Shared.Shared,\n task: Scheduler.PreparedTask,\n) -> () {\n let child = task.identity\n let mut links = LinkOutput { link: NoLink {} }\n let found = HashMap.withMut(&mut driver.tasks, parent, copyFirstChild(&mut links))\n let LinkOutput { link } = move links\n let entry = makeEntry(move task, move link)\n let mut allocator = Allocator.systemAllocatorProvider()\n let inserted = HashMap.insert(&mut driver.tasks, child, move entry)\n |> Effect.provideMut(&mut allocator)\n let outcome = run Effect.result(move inserted)\n return finishAdoption(\n move driver,\n parent,\n child,\n move wake,\n move response,\n move outcome,\n )\n}\n\nfn finishAdoption(\n driver: &mut Driver,\n parent: Scheduler.TaskId,\n child: Scheduler.TaskId,\n wake: Intrinsic.Wake,\n response: Shared.Shared,\n outcome: Result, OutOfMemoryError>,\n) -> () {\n let Result, OutOfMemoryError> { value } = move outcome\n return match move value {\n Success> { value: previous } => finishAcceptedAdoption(\n move driver,\n parent,\n child,\n move wake,\n move response,\n move previous,\n )\n Failure { error } => finishRejectedAdoption(\n move response,\n move wake,\n move error,\n )\n }\n}\n\nfn finishAcceptedAdoption(\n driver: &mut Driver,\n parent: Scheduler.TaskId,\n child: Scheduler.TaskId,\n wake: Intrinsic.Wake,\n response: Shared.Shared,\n previous: Option,\n) -> () {\n drop previous\n let linked = HashMap.withMut(&mut driver.tasks, parent, setFirstChild(child))\n let published = Shared.withMut(&response, markPublished)\n Intrinsic.wake(move wake)\n let notified = HashMap.withMut(&mut driver.tasks, child, notifyInitialEntry)\n drop response\n return ()\n}\n\nfn finishRejectedAdoption(\n response: Shared.Shared,\n wake: Intrinsic.Wake,\n error: OutOfMemoryError,\n) -> () {\n drop error\n let rejected = Shared.withMut(&response, markRejected)\n drop response\n Intrinsic.wake(move wake)\n return ()\n}\n\neffect fn processPublication(driver: &mut Driver, identity: Scheduler.TaskId) -> () {\n let mut handles = EntryHandles {\n mailbox: NoMailbox {},\n submission: NoSubmissionHandle {},\n }\n let found = HashMap.withMut(&mut driver.tasks, identity, copyHandles(&mut handles))\n if !found { drop handles return () }\n let EntryHandles { mailbox: selectedMailbox, submission: selectedSubmission } = move handles\n let mailbox = match move selectedMailbox {\n NoMailbox {} => missingMailbox()\n MailboxHandle { value } => move value\n }\n let submission = match move selectedSubmission {\n NoSubmissionHandle {} => missingSubmission()\n SubmissionHandle { value } => move value\n }\n let response = Shared.with<\n Scheduler.TaskMailbox,\n Shared.Shared,\n >(\n &mailbox,\n cloneResponse,\n )\n let request = Shared.withMut(&mailbox, takeRequest)\n let phase = Shared.withMut(&submission, takeSubmission)\n drop mailbox\n drop submission\n return match move request {\n Scheduler.NoRequest {} => finishNoRequest(move response, move phase)\n Scheduler.PendingRequest { wake } => match move phase {\n Scheduler.NoSubmission {} => finishMissingSubmission(move response, move wake)\n Scheduler.PendingSubmission { task } => run adopt(\n move driver,\n identity,\n move wake,\n move response,\n move task,\n )\n }\n }\n}\n\nfn cloneResponse(\n mailbox: &Scheduler.TaskMailbox,\n) -> Shared.Shared {\n return Shared.clone(&mailbox.response)\n}\n\nfn missingMailbox() -> Shared.Shared {\n let invalid = 1 / 0\n return missingMailbox()\n}\n\nfn missingSubmission() -> Shared.Shared {\n let invalid = 1 / 0\n return missingSubmission()\n}\n\nfn finishNoRequest(\n response: Shared.Shared,\n phase: Scheduler.NoSubmission | Scheduler.PendingSubmission,\n) -> () {\n drop response\n drop phase\n return ()\n}\n\nfn finishMissingSubmission(\n response: Shared.Shared,\n wake: Intrinsic.Wake,\n) -> () {\n drop response\n Intrinsic.wake(move wake)\n return ()\n}\n\nstruct CompletionAudit { completed: bool }\nfn phaseCompleted(entry: &mut TaskEntry, output: &mut CompletionAudit) -> () {\n output.completed = match &entry.phase { Completed {} => true _ => false }\n return ()\n}\n\nfn releaseEntry(entry: TaskEntry) -> () { drop entry return () }\nfn releaseRemoved(selected: Option) -> () {\n return match move selected {\n None {} => ()\n Some { value } => releaseEntry(move value)\n }\n}\n\nstruct TaskLinks {\n parent: NoLink | TaskLink\n firstChild: NoLink | TaskLink\n nextSibling: NoLink | TaskLink\n}\n\nfn copyTaskLinks(entry: &mut TaskEntry, output: &mut TaskLinks) -> () {\n output.parent = entry.parent\n output.firstChild = entry.firstChild\n output.nextSibling = entry.nextSibling\n return ()\n}\n\nfn readTaskLinks(\n tasks: &mut HashMap.HashMap,\n identity: Scheduler.TaskId,\n output: &mut TaskLinks,\n) -> bool {\n return HashMap.withMut(move tasks, identity, copyTaskLinks(move output))\n}\n\nfn installCancellationLink(entry: &mut TaskEntry, next: NoLink | TaskLink) -> () {\n entry.cancellationNext = move next\n return ()\n}\n\nfn takeCancellationLink(entry: &mut TaskEntry, output: &mut LinkOutput) -> () {\n output.link = Intrinsic.replace(entry.cancellationNext, NoLink {})\n return ()\n}\n\nfn setFirstChildLink(entry: &mut TaskEntry, link: NoLink | TaskLink) -> () {\n entry.firstChild = move link\n return ()\n}\n\nfn setNextSiblingLink(entry: &mut TaskEntry, link: NoLink | TaskLink) -> () {\n entry.nextSibling = move link\n return ()\n}\n\nfn hasLink(link: NoLink | TaskLink) -> bool {\n return match move link { NoLink {} => false TaskLink { identity } => true }\n}\n\nfn requireIdentity(link: NoLink | TaskLink) -> Scheduler.TaskId {\n return match move link {\n NoLink {} => missingIdentity()\n TaskLink { identity } => identity\n }\n}\n\nfn missingIdentity() -> Scheduler.TaskId {\n let invalid = 1 / 0\n return missingIdentity()\n}\n\nfn cancelEntry(entry: TaskEntry) -> () {\n let TaskEntry {\n parent,\n firstChild,\n nextSibling,\n cancellationNext,\n phase,\n execution,\n canceller,\n mailbox,\n submission,\n } = move entry\n Fiber.cancel(move canceller)\n drop parent\n drop firstChild\n drop nextSibling\n drop cancellationNext\n drop phase\n drop execution\n drop mailbox\n drop submission\n return ()\n}\n\nfn cancelRemoved(selected: Option) -> () {\n return match move selected {\n None {} => ()\n Some { value } => cancelEntry(move value)\n }\n}\n\nfn finishCompletedTask(driver: &mut Driver, identity: Scheduler.TaskId) -> () {\n let mut links = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let found = readTaskLinks(&mut driver.tasks, identity, &mut links)\n if !found { return () }\n\n let mut child = links.firstChild\n while hasLink(child) {\n let childIdentity = requireIdentity(child)\n let mut childLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let childFound = readTaskLinks(\n &mut driver.tasks,\n childIdentity,\n &mut childLinks,\n )\n if childFound {\n child = childLinks.nextSibling\n let nextWork = driver.cancellationHead\n let linked = HashMap.withMut(\n &mut driver.tasks,\n childIdentity,\n installCancellationLink(nextWork),\n )\n if linked { driver.cancellationHead = TaskLink { identity: childIdentity } }\n } else {\n child = NoLink {}\n }\n }\n\n let parent = links.parent\n if hasLink(parent) {\n let parentIdentity = requireIdentity(parent)\n let mut parentLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let parentFound = readTaskLinks(\n &mut driver.tasks,\n parentIdentity,\n &mut parentLinks,\n )\n if parentFound {\n let mut current = parentLinks.firstChild\n let mut previous = LinkOutput { link: NoLink {} }\n let mut unlinked = false\n while !unlinked && hasLink(current) {\n let currentIdentity = requireIdentity(current)\n let mut currentLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let currentFound = readTaskLinks(\n &mut driver.tasks,\n currentIdentity,\n &mut currentLinks,\n )\n if !currentFound {\n unlinked = true\n } else if currentIdentity.value == identity.value {\n let nextSibling = currentLinks.nextSibling\n let replaced = match previous.link {\n NoLink {} => HashMap.withMut(\n &mut driver.tasks,\n parentIdentity,\n setFirstChildLink(nextSibling),\n )\n TaskLink { identity: previousIdentity } => HashMap.withMut(\n &mut driver.tasks,\n previousIdentity,\n setNextSiblingLink(nextSibling),\n )\n }\n unlinked = true\n } else {\n previous.link = TaskLink { identity: currentIdentity }\n current = currentLinks.nextSibling\n }\n }\n }\n }\n\n let removed = HashMap.remove(&mut driver.tasks, identity)\n releaseRemoved(move removed)\n if identity.value == 0 { driver.rootComplete = true }\n\n while hasLink(driver.cancellationHead) {\n let selected = requireIdentity(driver.cancellationHead)\n let mut selectedLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let selectedFound = readTaskLinks(\n &mut driver.tasks,\n selected,\n &mut selectedLinks,\n )\n if !selectedFound {\n driver.cancellationHead = NoLink {}\n } else {\n let mut nextWork = LinkOutput { link: NoLink {} }\n let popped = HashMap.withMut(\n &mut driver.tasks,\n selected,\n takeCancellationLink(&mut nextWork),\n )\n driver.cancellationHead = nextWork.link\n let mut descendant = selectedLinks.firstChild\n while hasLink(descendant) {\n let descendantIdentity = requireIdentity(descendant)\n let mut descendantLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let descendantFound = readTaskLinks(\n &mut driver.tasks,\n descendantIdentity,\n &mut descendantLinks,\n )\n if descendantFound {\n descendant = descendantLinks.nextSibling\n let nextDescendant = driver.cancellationHead\n let linked = HashMap.withMut(\n &mut driver.tasks,\n descendantIdentity,\n installCancellationLink(nextDescendant),\n )\n if linked {\n driver.cancellationHead = TaskLink { identity: descendantIdentity }\n }\n } else {\n descendant = NoLink {}\n }\n }\n let cancelled = HashMap.remove(&mut driver.tasks, selected)\n cancelRemoved(move cancelled)\n }\n }\n return ()\n}\n\nfn cancelTree(driver: &mut Driver, identity: Scheduler.TaskId) -> () {\n let nextWork = driver.cancellationHead\n let linked = HashMap.withMut(\n &mut driver.tasks,\n identity,\n installCancellationLink(nextWork),\n )\n if linked { driver.cancellationHead = TaskLink { identity: identity } }\n\n while hasLink(driver.cancellationHead) {\n let selected = requireIdentity(driver.cancellationHead)\n let mut selectedLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let selectedFound = readTaskLinks(\n &mut driver.tasks,\n selected,\n &mut selectedLinks,\n )\n if !selectedFound {\n driver.cancellationHead = NoLink {}\n } else {\n let mut selectedWork = LinkOutput { link: NoLink {} }\n let popped = HashMap.withMut(\n &mut driver.tasks,\n selected,\n takeCancellationLink(&mut selectedWork),\n )\n driver.cancellationHead = selectedWork.link\n let mut descendant = selectedLinks.firstChild\n while hasLink(descendant) {\n let descendantIdentity = requireIdentity(descendant)\n let mut descendantLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let descendantFound = readTaskLinks(\n &mut driver.tasks,\n descendantIdentity,\n &mut descendantLinks,\n )\n if descendantFound {\n descendant = descendantLinks.nextSibling\n let nextDescendant = driver.cancellationHead\n let linkedDescendant = HashMap.withMut(\n &mut driver.tasks,\n descendantIdentity,\n installCancellationLink(nextDescendant),\n )\n if linkedDescendant {\n driver.cancellationHead = TaskLink { identity: descendantIdentity }\n }\n } else {\n descendant = NoLink {}\n }\n }\n let cancelled = HashMap.remove(&mut driver.tasks, selected)\n cancelRemoved(move cancelled)\n }\n }\n return ()\n}\n\neffect fn driveIdentity(driver: &mut Driver, identity: Scheduler.TaskId) -> ()\n! OutOfMemoryError {\n let eligible = HashMap.withMut(&mut driver.tasks, identity, markEligible)\n if !eligible { return () }\n let mut extraction = Extraction { slot: NoExecution {} }\n let found = HashMap.withMut(&mut driver.tasks, identity, extract(&mut extraction))\n if !found { drop extraction return () }\n driver.current = identity\n let Extraction { slot } = move extraction\n let mut state = ActivationState { slot: NoExecution {}, completed: false }\n run driveSelected(move slot, &mut state)\n let ActivationState { slot: parked, completed: parkedComplete } = move state\n let restored = match move parked {\n NoExecution {} => false\n StoredExecution { execution } => HashMap.withMut(\n &mut driver.tasks,\n identity,\n restore(move execution),\n )\n }\n if parkedComplete {\n let marked = HashMap.withMut(&mut driver.tasks, identity, markCompleted(()))\n }\n let mut terminal = CompletionAudit { completed: false }\n let inspected = HashMap.withMut(&mut driver.tasks, identity, phaseCompleted(&mut terminal))\n return run finishDrive(move driver, identity, terminal.completed)\n}\n\neffect fn finishDrive(\n driver: &mut Driver,\n identity: Scheduler.TaskId,\n terminal: bool,\n) -> () ! OutOfMemoryError {\n if !terminal { return run processPublication(move driver, identity) }\n let cleaned = finishCompletedTask(move driver, identity)\n drop cleaned\n return ()\n}\n\neffect fn stalledDriver(driver: Driver) -> Driver ! StalledError {\n drop driver\n fail StalledError {}\n}\n\nfn noReady() -> bool { return false }\n\neffect fn drivePresent(driver: &mut Driver, identity: Scheduler.TaskId) -> bool\n! OutOfMemoryError {\n run driveIdentity(move driver, identity)\n return true\n}\n\neffect fn driveReady(driver: &mut Driver, selected: QueueEmpty | ReadyIdentity) -> bool\n! OutOfMemoryError {\n return match move selected {\n QueueEmpty {} => noReady()\n ReadyIdentity { identity } => run drivePresent(move driver, identity)\n }\n}\n\neffect fn driveUntilRoot(driver: Driver) -> Driver\n! OutOfMemoryError | StalledError {\n let mut owned = move driver\n while !owned.rootComplete {\n let selected = takeReady(&owned.queue)\n let progressed = run driveReady(&mut owned, move selected)\n if !progressed {\n let cancelled = cancelTree(&mut owned, Scheduler.TaskId { value: 0 })\n drop cancelled\n return run stalledDriver(move owned)\n }\n }\n return move owned\n}\n\neffect fn finishRoot(selected: RootPending | RootReady) -> A ! E {\n return match move selected {\n RootPending {} => missingRoot()\n RootReady { result } => run finishRootResult(move result)\n }\n}\n\neffect fn finishRootResult(result: Result) -> A ! E {\n let Result { value } = move result\n return match move value {\n Success { value: success } => move success\n Failure { error } => run raiseRoot(move error)\n }\n}\n\neffect fn raiseRoot(error: E) -> A ! E { fail move error }\nfn missingRoot() -> A { let invalid = 1 / 0 return missingRoot() }\n\n/// Runs one lazy root program under this Scheduler and returns its typed outcome.\n///\n/// # Details\n///\n/// The root becomes task zero. This operation owns all per-run task storage, provides one distinct\n/// Scheduler client to each task, and dispatches ready tasks in FIFO order. Before it returns a\n/// root value or raises a typed error, it cancels every unfinished descendant and releases the\n/// run state. If the root is incomplete when no task is ready, it performs the same cleanup and\n/// raises [`StalledError`].\npub effect fn execute(\n self: &mut LocalScheduler,\n program: once Effect,\n) -> A\n! E | OutOfMemoryError | StalledError {\n let mut allocator = Allocator.systemAllocatorProvider()\n let queue = run Shared.make(ReadyQueue {\n head: NoLink {},\n tail: NoLink {},\n }) |> Effect.provideMut(&mut allocator)\n let identities = run Shared.make(TaskIdSource {\n next: 1,\n exhausted: false,\n }) |> Effect.provideMut(&mut allocator)\n let prepared = run Fiber.prepare()\n |> Effect.provideMut(&mut allocator)\n let rootState = run Shared.make>(RootState {\n phase: RootPending {},\n }) |> Effect.provideMut(&mut allocator)\n let response = run Shared.make(Scheduler.PublicationResponse {\n phase: Scheduler.ResponseWaiting {},\n }) |> Effect.provideMut(&mut allocator)\n let submission = run Shared.make(Scheduler.SubmissionSlot {\n phase: Scheduler.NoSubmission {},\n }) |> Effect.provideMut(&mut allocator)\n let mailboxResponse = Shared.clone(&response)\n let mailbox = run Shared.make(Scheduler.TaskMailbox {\n request: Scheduler.NoRequest {},\n response: move mailboxResponse,\n }) |> Effect.provideMut(&mut allocator)\n let root = Scheduler.TaskId { value: 0 }\n let node = run Shared.make(ReadyNode {\n identity: root,\n next: NoLink {},\n enqueued: false,\n }) |> Effect.provideMut(&mut allocator)\n let Fiber.PreparedFiber {\n producer,\n canceller,\n fiber: rootFiber,\n } = move prepared\n drop rootFiber\n let client = SchedulerClient {\n identity: root,\n mailbox: Shared.clone(&mailbox),\n submission: Shared.clone(&submission),\n response: move response,\n identities: move identities,\n queue: Shared.clone(&queue),\n }\n let endpoint = ReadyEndpoint {\n queue: Shared.clone(&queue),\n node: move node,\n }\n let execution = run Execution.make(\n runRoot(\n move program,\n move client,\n Shared.clone>(&rootState),\n move producer,\n ),\n move endpoint,\n notifyReady,\n ) |> Effect.provideMut(&mut allocator)\n let entry = TaskEntry {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n cancellationNext: NoLink {},\n phase: Initial {},\n execution: StoredExecution { execution: move execution },\n canceller: move canceller,\n mailbox: move mailbox,\n submission: move submission,\n }\n let mut driver = Driver {\n tasks: HashMap.make(Hash.seed(0)),\n queue: move queue,\n current: root,\n cancellationHead: NoLink {},\n rootComplete: false,\n }\n let inserted = run HashMap.insert(\n &mut driver.tasks,\n root,\n move entry,\n ) |> Effect.provideMut(&mut allocator)\n drop inserted\n let notified = HashMap.withMut(&mut driver.tasks, root, notifyInitialEntry)\n let completed = run driveUntilRoot(move driver)\n drop completed\n let mut extraction = RootExtraction { phase: RootPending {} }\n let extracted = Shared.withMut(&rootState, extractRoot(&mut extraction))\n let RootExtraction { phase: selected } = move extraction\n drop rootState\n return run finishRoot(move selected)\n}\n', + '//! Deterministic single-threaded execution for structured Fibers.\n//!\n//! # When to use\n//! Use [`execute`] at an application entry point to run one lazy program that uses the\n//! `silk.scheduler.Scheduler` service.\n//!\n//! # Details\n//! Each call creates fresh task storage and a FIFO ready queue. The root is task zero and uses the\n//! same `Execution<()>` storage as every child. [`execute`] returns only after the root terminates.\n\nimport silk.allocator { Allocator, OutOfMemoryError }\nimport silk.effect as Effect\nimport silk.execution as Execution\nimport silk.fiber as Fiber\nimport silk.hash as Hash\nimport silk.hash_map as HashMap\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.scheduler as Scheduler\nimport silk.shared as Shared\nimport silk.u64 as u64\n\n/// A reusable deterministic single-threaded Scheduler provider.\n///\n/// # Details\n///\n/// Each call to [`execute`] creates fresh task storage and readiness state.\npub struct LocalScheduler {}\n\n/// Reports that no task is ready while the root is incomplete.\n///\n/// # Details\n///\n/// [`execute`] cancels and releases the incomplete task tree before it raises this error.\npub struct StalledError {}\n\n/// Constructs a reusable local Scheduler value.\npub fn make() -> LocalScheduler {\n return LocalScheduler {}\n}\n\nstruct NoLink {}\nstruct TaskLink { identity: Scheduler.TaskId }\nimpl Copy for NoLink {}\nimpl Copy for TaskLink {}\n\nstruct ReadyNode {\n identity: Scheduler.TaskId\n next: NoLink | ReadyNodeLink\n enqueued: bool\n}\n\nstruct ReadyNodeLink { node: Shared.Shared }\nstruct ReadyQueue {\n head: NoLink | ReadyNodeLink\n tail: NoLink | ReadyNodeLink\n}\n\nstruct ReadyEndpoint {\n queue: Shared.Shared\n node: Shared.Shared\n}\n\nfn claimReady(node: &mut ReadyNode) -> bool {\n if node.enqueued { return false }\n node.enqueued = true\n return true\n}\n\nfn linkReady(node: &mut ReadyNode, next: Shared.Shared) -> () {\n let previous = Intrinsic.replace(node.next, ReadyNodeLink { node: move next })\n drop previous\n return ()\n}\n\nfn installFirst(queue: &mut ReadyQueue, node: Shared.Shared) -> () {\n queue.head = ReadyNodeLink { node: move node }\n return ()\n}\n\nfn installAfter(previous: Shared.Shared, node: Shared.Shared) -> () {\n let installed = Shared.withMut(&previous, linkReady(move node))\n drop previous\n return ()\n}\n\nfn appendReady(queue: &mut ReadyQueue, node: Shared.Shared) -> () {\n let accepted = Shared.withMut(&node, claimReady)\n if !accepted { drop node return () }\n let retained = Shared.clone(&node)\n let previous = Intrinsic.replace(queue.tail, ReadyNodeLink { node: move retained })\n return match move previous {\n NoLink {} => installFirst(move queue, move node)\n ReadyNodeLink { node: tail } => installAfter(move tail, move node)\n }\n}\n\nfn notifyReady(endpoint: &ReadyEndpoint) -> () {\n let queue = Shared.clone(&endpoint.queue)\n let node = Shared.clone(&endpoint.node)\n let appended = Shared.withMut(&queue, appendReady(move node))\n drop queue\n return ()\n}\n\nfn takeNext(node: &mut ReadyNode) -> NoLink | ReadyNodeLink {\n return Intrinsic.replace(node.next, NoLink {})\n}\n\nfn clearTail(queue: &mut ReadyQueue) -> () {\n let previous = Intrinsic.replace(queue.tail, NoLink {})\n drop previous\n return ()\n}\n\nfn advanceHead(queue: &mut ReadyQueue, next: NoLink | ReadyNodeLink) -> () {\n return match move next {\n NoLink {} => clearTail(move queue)\n ReadyNodeLink { node } => installFirst(move queue, move node)\n }\n}\n\nfn takeLinked(\n queue: &mut ReadyQueue,\n node: Shared.Shared,\n) -> NoLink | ReadyNodeLink {\n let next = Shared.withMut(&node, takeNext)\n advanceHead(move queue, move next)\n return ReadyNodeLink { node: move node }\n}\n\nfn takeHead(queue: &mut ReadyQueue) -> NoLink | ReadyNodeLink {\n let selected = Intrinsic.replace(queue.head, NoLink {})\n return match move selected {\n NoLink {} => NoLink {}\n ReadyNodeLink { node } => takeLinked(move queue, move node)\n }\n}\n\nfn releaseReady(node: &mut ReadyNode) -> Scheduler.TaskId {\n node.enqueued = false\n return node.identity\n}\n\nstruct QueueEmpty {}\nstruct ReadyIdentity { identity: Scheduler.TaskId }\n\nfn releaseSelected(node: Shared.Shared) -> ReadyIdentity {\n let identity = Shared.withMut(&node, releaseReady)\n drop node\n return ReadyIdentity { identity: identity }\n}\n\nfn takeReady(queue: &Shared.Shared) -> QueueEmpty | ReadyIdentity {\n let selected = Shared.withMut(queue, takeHead)\n return match move selected {\n NoLink {} => QueueEmpty {}\n ReadyNodeLink { node } => releaseSelected(move node)\n }\n}\n\nstruct TaskIdSource { next: u64 exhausted: bool }\nstruct Reserved { identity: Scheduler.TaskId }\nstruct Refused {}\n\nfn reserveStep(source: &mut TaskIdSource) -> Reserved | Refused {\n if source.exhausted { return Refused {} }\n let selected = source.next\n if selected == u64.MAX { source.exhausted = true } else { source.next = selected + 1 }\n return Reserved { identity: Scheduler.TaskId { value: selected } }\n}\n\neffect fn exhausted() -> Scheduler.TaskId ! Scheduler.TaskIdExhaustedError {\n fail Scheduler.TaskIdExhaustedError {}\n}\n\neffect fn reserve(source: &Shared.Shared) -> Scheduler.TaskId\n! Scheduler.TaskIdExhaustedError {\n let selected = Shared.withMut(source, reserveStep)\n return match move selected {\n Reserved { identity } => move identity\n Refused {} => run exhausted()\n }\n}\n\nstruct SchedulerClient {\n identity: Scheduler.TaskId\n mailbox: Shared.Shared\n submission: Shared.Shared\n response: Shared.Shared\n identities: Shared.Shared\n queue: Shared.Shared\n}\n\nfn publishTaskResult(\n result: Result,\n producer: Fiber.CompletionProducer,\n) -> () {\n return match move result {\n Result.Success { value: success } => Fiber.completeSuccess(move producer, move success)\n Result.Failure { error } => Fiber.completeFailure(move producer, move error)\n }\n}\n\neffect fn runTask(\n body: once Effect,\n client: SchedulerClient,\n producer: Fiber.CompletionProducer,\n) -> () {\n let result = run Effect.bindRequirementOwned(\n Effect.result(move body),\n move client,\n )\n publishTaskResult(move result, move producer)\n return ()\n}\n\nstruct RootPending {}\nstruct RootReady { result: Result }\nstruct RootState { phase: RootPending | RootReady }\nstruct RootTaskCompleted {}\nstruct RootTaskNeverFailed {}\n\nfn storeRoot(state: &mut RootState, result: Result) -> () {\n let previous = Intrinsic.replace(state.phase, RootReady { result: move result })\n drop previous\n return ()\n}\n\nfn takeRoot(state: &mut RootState) -> RootPending | RootReady {\n return Intrinsic.replace(state.phase, RootPending {})\n}\n\nstruct RootExtraction { phase: RootPending | RootReady }\n\nfn extractRoot(state: &mut RootState, output: &mut RootExtraction) -> () {\n output.phase = Intrinsic.replace(state.phase, RootPending {})\n return ()\n}\n\neffect fn runRoot(\n body: once Effect,\n client: SchedulerClient,\n state: Shared.Shared>,\n producer: Fiber.CompletionProducer,\n) -> () {\n let result = run Effect.bindRequirementOwned(\n Effect.result(move body),\n move client,\n )\n let stored = Shared.withMut(&state, storeRoot(move result))\n drop state\n Fiber.completeSuccess(\n move producer,\n RootTaskCompleted {},\n )\n return ()\n}\n\neffect fn prepareChild(\n self: &mut SchedulerClient,\n child: once Effect,\n) -> Scheduler.PendingPublication\n! OutOfMemoryError | Scheduler.TaskIdExhaustedError {\n let identity = run reserve(&self.identities)\n let mut allocator = Allocator.systemAllocatorProvider()\n let prepared = run Fiber.prepare() |> Effect.provideMut(&mut allocator)\n let response = run Shared.make(Scheduler.PublicationResponse {\n phase: Scheduler.ResponseWaiting {},\n }) |> Effect.provideMut(&mut allocator)\n let childSubmission = run Shared.make(Scheduler.SubmissionSlot {\n phase: Scheduler.NoSubmission {},\n }) |> Effect.provideMut(&mut allocator)\n let mailboxResponse = Shared.clone(&response)\n let childMailbox = run Shared.make(Scheduler.TaskMailbox {\n request: Scheduler.NoRequest {},\n response: move mailboxResponse,\n }) |> Effect.provideMut(&mut allocator)\n let node = run Shared.make(ReadyNode {\n identity: identity,\n next: NoLink {},\n enqueued: false,\n }) |> Effect.provideMut(&mut allocator)\n let Fiber.PreparedFiber { producer, canceller, fiber } = move prepared\n let client = SchedulerClient {\n identity: identity,\n mailbox: Shared.clone(&childMailbox),\n submission: Shared.clone(&childSubmission),\n response: move response,\n identities: Shared.clone(&self.identities),\n queue: Shared.clone(&self.queue),\n }\n let endpoint = ReadyEndpoint {\n queue: Shared.clone(&self.queue),\n node: move node,\n }\n let execution = run Execution.make(\n runTask(move child, move client, move producer),\n move endpoint,\n notifyReady,\n ) |> Effect.provideMut(&mut allocator)\n let task = Scheduler.PreparedTask {\n identity: identity,\n parent: self.identity,\n execution: move execution,\n canceller: move canceller,\n mailbox: move childMailbox,\n submission: move childSubmission,\n }\n return Scheduler.pending(\n move fiber,\n Shared.clone(&self.mailbox),\n Shared.clone(&self.submission),\n Shared.clone(&self.response),\n move task,\n )\n}\n\nimpl Scheduler.Scheduler for SchedulerClient { prepare: SchedulerClient.prepareChild }\n\nstruct Initial {}\nstruct Running {}\nstruct Dormant {}\nstruct Eligible {}\nstruct Completed {}\nstruct NoExecution {}\nstruct StoredExecution { execution: Intrinsic.Execution<()> }\n\nstruct TaskEntry {\n parent: NoLink | TaskLink\n firstChild: NoLink | TaskLink\n nextSibling: NoLink | TaskLink\n cancellationNext: NoLink | TaskLink\n phase: Initial | Running | Dormant | Eligible | Completed\n execution: NoExecution | StoredExecution\n canceller: Fiber.CompletionCanceller\n mailbox: Shared.Shared\n submission: Shared.Shared\n}\n\nstruct Driver {\n tasks: HashMap.HashMap\n queue: Shared.Shared\n current: Scheduler.TaskId\n cancellationHead: NoLink | TaskLink\n rootComplete: bool\n}\n\nstruct Extraction { slot: NoExecution | StoredExecution }\n\nfn extract(entry: &mut TaskEntry, output: &mut Extraction) -> () {\n output.slot = Intrinsic.replace(entry.execution, NoExecution {})\n entry.phase = Running {}\n return ()\n}\n\nfn markEligible(entry: &mut TaskEntry) -> () {\n entry.phase = Eligible {}\n return ()\n}\n\nfn restore(entry: &mut TaskEntry, execution: Intrinsic.Execution<()>) -> () {\n entry.execution = StoredExecution { execution: move execution }\n entry.phase = Dormant {}\n return ()\n}\n\nfn notifyStoredInitial(\n entry: &mut TaskEntry,\n execution: Intrinsic.Execution<()>,\n) -> () {\n let mut selected = move execution\n Execution.notifyInitial(&mut selected)\n entry.execution = StoredExecution { execution: move selected }\n entry.phase = Eligible {}\n return ()\n}\n\nfn notifyInitialEntry(entry: &mut TaskEntry) -> () {\n let selected = Intrinsic.replace(entry.execution, NoExecution {})\n return match move selected {\n NoExecution {} => missingInitialExecution()\n StoredExecution { execution } => notifyStoredInitial(move entry, move execution)\n }\n}\n\nfn missingInitialExecution() -> () {\n let invalid = 1 / 0\n return missingInitialExecution()\n}\n\nfn markCompleted(entry: &mut TaskEntry, result: ()) -> () {\n drop result\n entry.phase = Completed {}\n return ()\n}\n\nstruct ActivationState {\n slot: NoExecution | StoredExecution\n completed: bool\n}\n\nfn suspended(state: &mut ActivationState, execution: Intrinsic.Execution<()>) -> () {\n state.slot = StoredExecution { execution: move execution }\n return ()\n}\n\nfn completed(state: &mut ActivationState, result: ()) -> () {\n drop result\n state.completed = true\n return ()\n}\n\neffect fn driveSelected(\n selected: NoExecution | StoredExecution,\n state: &mut ActivationState,\n) -> () {\n return match move selected {\n NoExecution {} => ()\n StoredExecution { execution } => run Execution.drive(\n move execution,\n move state,\n completed,\n suspended,\n )\n }\n}\n\nfn takeRequest(\n mailbox: &mut Scheduler.TaskMailbox,\n) -> Scheduler.NoRequest | Scheduler.PendingRequest {\n return Intrinsic.replace(mailbox.request, Scheduler.NoRequest {})\n}\n\nfn takeSubmission(\n slot: &mut Scheduler.SubmissionSlot,\n) -> Scheduler.NoSubmission | Scheduler.PendingSubmission {\n return Intrinsic.replace(slot.phase, Scheduler.NoSubmission {})\n}\n\nstruct NoMailbox {}\nstruct MailboxHandle { value: Shared.Shared }\nstruct NoSubmissionHandle {}\nstruct SubmissionHandle { value: Shared.Shared }\nstruct EntryHandles {\n mailbox: NoMailbox | MailboxHandle\n submission: NoSubmissionHandle | SubmissionHandle\n}\n\nfn copyHandles(entry: &mut TaskEntry, output: &mut EntryHandles) -> () {\n output.mailbox = MailboxHandle {\n value: Shared.clone(&entry.mailbox),\n }\n output.submission = SubmissionHandle {\n value: Shared.clone(&entry.submission),\n }\n return ()\n}\n\nfn markPublished(response: &mut Scheduler.PublicationResponse) -> () {\n response.phase = Scheduler.ResponsePublished {}\n return ()\n}\n\nfn markRejected(response: &mut Scheduler.PublicationResponse) -> () {\n response.phase = Scheduler.ResponseRejected {}\n return ()\n}\n\nfn setFirstChild(entry: &mut TaskEntry, child: Scheduler.TaskId) -> () {\n entry.firstChild = TaskLink { identity: child }\n return ()\n}\n\nfn makeEntry(task: Scheduler.PreparedTask, sibling: NoLink | TaskLink) -> TaskEntry {\n let Scheduler.PreparedTask {\n identity,\n parent,\n execution,\n canceller,\n mailbox,\n submission,\n } = move task\n return TaskEntry {\n parent: TaskLink { identity: parent },\n firstChild: NoLink {},\n nextSibling: move sibling,\n cancellationNext: NoLink {},\n phase: Initial {},\n execution: StoredExecution { execution: move execution },\n canceller: move canceller,\n mailbox: move mailbox,\n submission: move submission,\n }\n}\n\nstruct LinkOutput { link: NoLink | TaskLink }\nfn copyFirstChild(entry: &mut TaskEntry, output: &mut LinkOutput) -> () {\n output.link = entry.firstChild\n return ()\n}\n\neffect fn adopt(\n driver: &mut Driver,\n parent: Scheduler.TaskId,\n wake: Intrinsic.Wake,\n response: Shared.Shared,\n task: Scheduler.PreparedTask,\n) -> () {\n let child = task.identity\n let mut links = LinkOutput { link: NoLink {} }\n let found = HashMap.withMut(&mut driver.tasks, parent, copyFirstChild(&mut links))\n let LinkOutput { link } = move links\n let entry = makeEntry(move task, move link)\n let mut allocator = Allocator.systemAllocatorProvider()\n let inserted = HashMap.insert(&mut driver.tasks, child, move entry)\n |> Effect.provideMut(&mut allocator)\n let outcome = run Effect.result(move inserted)\n return finishAdoption(\n move driver,\n parent,\n child,\n move wake,\n move response,\n move outcome,\n )\n}\n\nfn finishAdoption(\n driver: &mut Driver,\n parent: Scheduler.TaskId,\n child: Scheduler.TaskId,\n wake: Intrinsic.Wake,\n response: Shared.Shared,\n outcome: Result, OutOfMemoryError>,\n) -> () {\n return match move outcome {\n Result, OutOfMemoryError>.Success { value: previous } => finishAcceptedAdoption(\n move driver,\n parent,\n child,\n move wake,\n move response,\n move previous,\n )\n Result, OutOfMemoryError>.Failure { error } => finishRejectedAdoption(\n move response,\n move wake,\n move error,\n )\n }\n}\n\nfn finishAcceptedAdoption(\n driver: &mut Driver,\n parent: Scheduler.TaskId,\n child: Scheduler.TaskId,\n wake: Intrinsic.Wake,\n response: Shared.Shared,\n previous: Option,\n) -> () {\n drop previous\n let linked = HashMap.withMut(&mut driver.tasks, parent, setFirstChild(child))\n let published = Shared.withMut(&response, markPublished)\n Intrinsic.wake(move wake)\n let notified = HashMap.withMut(&mut driver.tasks, child, notifyInitialEntry)\n drop response\n return ()\n}\n\nfn finishRejectedAdoption(\n response: Shared.Shared,\n wake: Intrinsic.Wake,\n error: OutOfMemoryError,\n) -> () {\n drop error\n let rejected = Shared.withMut(&response, markRejected)\n drop response\n Intrinsic.wake(move wake)\n return ()\n}\n\neffect fn processPublication(driver: &mut Driver, identity: Scheduler.TaskId) -> () {\n let mut handles = EntryHandles {\n mailbox: NoMailbox {},\n submission: NoSubmissionHandle {},\n }\n let found = HashMap.withMut(&mut driver.tasks, identity, copyHandles(&mut handles))\n if !found { drop handles return () }\n let EntryHandles { mailbox: selectedMailbox, submission: selectedSubmission } = move handles\n let mailbox = match move selectedMailbox {\n NoMailbox {} => missingMailbox()\n MailboxHandle { value } => move value\n }\n let submission = match move selectedSubmission {\n NoSubmissionHandle {} => missingSubmission()\n SubmissionHandle { value } => move value\n }\n let response = Shared.with<\n Scheduler.TaskMailbox,\n Shared.Shared,\n >(\n &mailbox,\n cloneResponse,\n )\n let request = Shared.withMut(&mailbox, takeRequest)\n let phase = Shared.withMut(&submission, takeSubmission)\n drop mailbox\n drop submission\n return match move request {\n Scheduler.NoRequest {} => finishNoRequest(move response, move phase)\n Scheduler.PendingRequest { wake } => match move phase {\n Scheduler.NoSubmission {} => finishMissingSubmission(move response, move wake)\n Scheduler.PendingSubmission { task } => run adopt(\n move driver,\n identity,\n move wake,\n move response,\n move task,\n )\n }\n }\n}\n\nfn cloneResponse(\n mailbox: &Scheduler.TaskMailbox,\n) -> Shared.Shared {\n return Shared.clone(&mailbox.response)\n}\n\nfn missingMailbox() -> Shared.Shared {\n let invalid = 1 / 0\n return missingMailbox()\n}\n\nfn missingSubmission() -> Shared.Shared {\n let invalid = 1 / 0\n return missingSubmission()\n}\n\nfn finishNoRequest(\n response: Shared.Shared,\n phase: Scheduler.NoSubmission | Scheduler.PendingSubmission,\n) -> () {\n drop response\n drop phase\n return ()\n}\n\nfn finishMissingSubmission(\n response: Shared.Shared,\n wake: Intrinsic.Wake,\n) -> () {\n drop response\n Intrinsic.wake(move wake)\n return ()\n}\n\nstruct CompletionAudit { completed: bool }\nfn phaseCompleted(entry: &mut TaskEntry, output: &mut CompletionAudit) -> () {\n output.completed = match &entry.phase { Completed {} => true _ => false }\n return ()\n}\n\nfn releaseEntry(entry: TaskEntry) -> () { drop entry return () }\nfn releaseRemoved(selected: Option) -> () {\n return match move selected {\n Option.None => ()\n Option.Some { value } => releaseEntry(move value)\n }\n}\n\nstruct TaskLinks {\n parent: NoLink | TaskLink\n firstChild: NoLink | TaskLink\n nextSibling: NoLink | TaskLink\n}\n\nfn copyTaskLinks(entry: &mut TaskEntry, output: &mut TaskLinks) -> () {\n output.parent = entry.parent\n output.firstChild = entry.firstChild\n output.nextSibling = entry.nextSibling\n return ()\n}\n\nfn readTaskLinks(\n tasks: &mut HashMap.HashMap,\n identity: Scheduler.TaskId,\n output: &mut TaskLinks,\n) -> bool {\n return HashMap.withMut(move tasks, identity, copyTaskLinks(move output))\n}\n\nfn installCancellationLink(entry: &mut TaskEntry, next: NoLink | TaskLink) -> () {\n entry.cancellationNext = move next\n return ()\n}\n\nfn takeCancellationLink(entry: &mut TaskEntry, output: &mut LinkOutput) -> () {\n output.link = Intrinsic.replace(entry.cancellationNext, NoLink {})\n return ()\n}\n\nfn setFirstChildLink(entry: &mut TaskEntry, link: NoLink | TaskLink) -> () {\n entry.firstChild = move link\n return ()\n}\n\nfn setNextSiblingLink(entry: &mut TaskEntry, link: NoLink | TaskLink) -> () {\n entry.nextSibling = move link\n return ()\n}\n\nfn hasLink(link: NoLink | TaskLink) -> bool {\n return match move link { NoLink {} => false TaskLink { identity } => true }\n}\n\nfn requireIdentity(link: NoLink | TaskLink) -> Scheduler.TaskId {\n return match move link {\n NoLink {} => missingIdentity()\n TaskLink { identity } => identity\n }\n}\n\nfn missingIdentity() -> Scheduler.TaskId {\n let invalid = 1 / 0\n return missingIdentity()\n}\n\nfn cancelEntry(entry: TaskEntry) -> () {\n let TaskEntry {\n parent,\n firstChild,\n nextSibling,\n cancellationNext,\n phase,\n execution,\n canceller,\n mailbox,\n submission,\n } = move entry\n Fiber.cancel(move canceller)\n drop parent\n drop firstChild\n drop nextSibling\n drop cancellationNext\n drop phase\n drop execution\n drop mailbox\n drop submission\n return ()\n}\n\nfn cancelRemoved(selected: Option) -> () {\n return match move selected {\n Option.None => ()\n Option.Some { value } => cancelEntry(move value)\n }\n}\n\nfn finishCompletedTask(driver: &mut Driver, identity: Scheduler.TaskId) -> () {\n let mut links = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let found = readTaskLinks(&mut driver.tasks, identity, &mut links)\n if !found { return () }\n\n let mut child = links.firstChild\n while hasLink(child) {\n let childIdentity = requireIdentity(child)\n let mut childLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let childFound = readTaskLinks(\n &mut driver.tasks,\n childIdentity,\n &mut childLinks,\n )\n if childFound {\n child = childLinks.nextSibling\n let nextWork = driver.cancellationHead\n let linked = HashMap.withMut(\n &mut driver.tasks,\n childIdentity,\n installCancellationLink(nextWork),\n )\n if linked { driver.cancellationHead = TaskLink { identity: childIdentity } }\n } else {\n child = NoLink {}\n }\n }\n\n let parent = links.parent\n if hasLink(parent) {\n let parentIdentity = requireIdentity(parent)\n let mut parentLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let parentFound = readTaskLinks(\n &mut driver.tasks,\n parentIdentity,\n &mut parentLinks,\n )\n if parentFound {\n let mut current = parentLinks.firstChild\n let mut previous = LinkOutput { link: NoLink {} }\n let mut unlinked = false\n while !unlinked && hasLink(current) {\n let currentIdentity = requireIdentity(current)\n let mut currentLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let currentFound = readTaskLinks(\n &mut driver.tasks,\n currentIdentity,\n &mut currentLinks,\n )\n if !currentFound {\n unlinked = true\n } else if currentIdentity.value == identity.value {\n let nextSibling = currentLinks.nextSibling\n let replaced = match previous.link {\n NoLink {} => HashMap.withMut(\n &mut driver.tasks,\n parentIdentity,\n setFirstChildLink(nextSibling),\n )\n TaskLink { identity: previousIdentity } => HashMap.withMut(\n &mut driver.tasks,\n previousIdentity,\n setNextSiblingLink(nextSibling),\n )\n }\n unlinked = true\n } else {\n previous.link = TaskLink { identity: currentIdentity }\n current = currentLinks.nextSibling\n }\n }\n }\n }\n\n let removed = HashMap.remove(&mut driver.tasks, identity)\n releaseRemoved(move removed)\n if identity.value == 0 { driver.rootComplete = true }\n\n while hasLink(driver.cancellationHead) {\n let selected = requireIdentity(driver.cancellationHead)\n let mut selectedLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let selectedFound = readTaskLinks(\n &mut driver.tasks,\n selected,\n &mut selectedLinks,\n )\n if !selectedFound {\n driver.cancellationHead = NoLink {}\n } else {\n let mut nextWork = LinkOutput { link: NoLink {} }\n let popped = HashMap.withMut(\n &mut driver.tasks,\n selected,\n takeCancellationLink(&mut nextWork),\n )\n driver.cancellationHead = nextWork.link\n let mut descendant = selectedLinks.firstChild\n while hasLink(descendant) {\n let descendantIdentity = requireIdentity(descendant)\n let mut descendantLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let descendantFound = readTaskLinks(\n &mut driver.tasks,\n descendantIdentity,\n &mut descendantLinks,\n )\n if descendantFound {\n descendant = descendantLinks.nextSibling\n let nextDescendant = driver.cancellationHead\n let linked = HashMap.withMut(\n &mut driver.tasks,\n descendantIdentity,\n installCancellationLink(nextDescendant),\n )\n if linked {\n driver.cancellationHead = TaskLink { identity: descendantIdentity }\n }\n } else {\n descendant = NoLink {}\n }\n }\n let cancelled = HashMap.remove(&mut driver.tasks, selected)\n cancelRemoved(move cancelled)\n }\n }\n return ()\n}\n\nfn cancelTree(driver: &mut Driver, identity: Scheduler.TaskId) -> () {\n let nextWork = driver.cancellationHead\n let linked = HashMap.withMut(\n &mut driver.tasks,\n identity,\n installCancellationLink(nextWork),\n )\n if linked { driver.cancellationHead = TaskLink { identity: identity } }\n\n while hasLink(driver.cancellationHead) {\n let selected = requireIdentity(driver.cancellationHead)\n let mut selectedLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let selectedFound = readTaskLinks(\n &mut driver.tasks,\n selected,\n &mut selectedLinks,\n )\n if !selectedFound {\n driver.cancellationHead = NoLink {}\n } else {\n let mut selectedWork = LinkOutput { link: NoLink {} }\n let popped = HashMap.withMut(\n &mut driver.tasks,\n selected,\n takeCancellationLink(&mut selectedWork),\n )\n driver.cancellationHead = selectedWork.link\n let mut descendant = selectedLinks.firstChild\n while hasLink(descendant) {\n let descendantIdentity = requireIdentity(descendant)\n let mut descendantLinks = TaskLinks {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n }\n let descendantFound = readTaskLinks(\n &mut driver.tasks,\n descendantIdentity,\n &mut descendantLinks,\n )\n if descendantFound {\n descendant = descendantLinks.nextSibling\n let nextDescendant = driver.cancellationHead\n let linkedDescendant = HashMap.withMut(\n &mut driver.tasks,\n descendantIdentity,\n installCancellationLink(nextDescendant),\n )\n if linkedDescendant {\n driver.cancellationHead = TaskLink { identity: descendantIdentity }\n }\n } else {\n descendant = NoLink {}\n }\n }\n let cancelled = HashMap.remove(&mut driver.tasks, selected)\n cancelRemoved(move cancelled)\n }\n }\n return ()\n}\n\neffect fn driveIdentity(driver: &mut Driver, identity: Scheduler.TaskId) -> ()\n! OutOfMemoryError {\n let eligible = HashMap.withMut(&mut driver.tasks, identity, markEligible)\n if !eligible { return () }\n let mut extraction = Extraction { slot: NoExecution {} }\n let found = HashMap.withMut(&mut driver.tasks, identity, extract(&mut extraction))\n if !found { drop extraction return () }\n driver.current = identity\n let Extraction { slot } = move extraction\n let mut state = ActivationState { slot: NoExecution {}, completed: false }\n run driveSelected(move slot, &mut state)\n let ActivationState { slot: parked, completed: parkedComplete } = move state\n let restored = match move parked {\n NoExecution {} => false\n StoredExecution { execution } => HashMap.withMut(\n &mut driver.tasks,\n identity,\n restore(move execution),\n )\n }\n if parkedComplete {\n let marked = HashMap.withMut(&mut driver.tasks, identity, markCompleted(()))\n }\n let mut terminal = CompletionAudit { completed: false }\n let inspected = HashMap.withMut(&mut driver.tasks, identity, phaseCompleted(&mut terminal))\n return run finishDrive(move driver, identity, terminal.completed)\n}\n\neffect fn finishDrive(\n driver: &mut Driver,\n identity: Scheduler.TaskId,\n terminal: bool,\n) -> () ! OutOfMemoryError {\n if !terminal { return run processPublication(move driver, identity) }\n let cleaned = finishCompletedTask(move driver, identity)\n drop cleaned\n return ()\n}\n\neffect fn stalledDriver(driver: Driver) -> Driver ! StalledError {\n drop driver\n fail StalledError {}\n}\n\nfn noReady() -> bool { return false }\n\neffect fn drivePresent(driver: &mut Driver, identity: Scheduler.TaskId) -> bool\n! OutOfMemoryError {\n run driveIdentity(move driver, identity)\n return true\n}\n\neffect fn driveReady(driver: &mut Driver, selected: QueueEmpty | ReadyIdentity) -> bool\n! OutOfMemoryError {\n return match move selected {\n QueueEmpty {} => noReady()\n ReadyIdentity { identity } => run drivePresent(move driver, identity)\n }\n}\n\neffect fn driveUntilRoot(driver: Driver) -> Driver\n! OutOfMemoryError | StalledError {\n let mut owned = move driver\n while !owned.rootComplete {\n let selected = takeReady(&owned.queue)\n let progressed = run driveReady(&mut owned, move selected)\n if !progressed {\n let cancelled = cancelTree(&mut owned, Scheduler.TaskId { value: 0 })\n drop cancelled\n return run stalledDriver(move owned)\n }\n }\n return move owned\n}\n\neffect fn finishRoot(selected: RootPending | RootReady) -> A ! E {\n return match move selected {\n RootPending {} => missingRoot()\n RootReady { result } => run finishRootResult(move result)\n }\n}\n\neffect fn finishRootResult(result: Result) -> A ! E {\n return match move result {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raiseRoot(move error)\n }\n}\n\neffect fn raiseRoot(error: E) -> A ! E { fail move error }\nfn missingRoot() -> A { let invalid = 1 / 0 return missingRoot() }\n\n/// Runs one lazy root program under this Scheduler and returns its typed outcome.\n///\n/// # Details\n///\n/// The root becomes task zero. This operation owns all per-run task storage, provides one distinct\n/// Scheduler client to each task, and dispatches ready tasks in FIFO order. Before it returns a\n/// root value or raises a typed error, it cancels every unfinished descendant and releases the\n/// run state. If the root is incomplete when no task is ready, it performs the same cleanup and\n/// raises [`StalledError`].\npub effect fn execute(\n self: &mut LocalScheduler,\n program: once Effect,\n) -> A\n! E | OutOfMemoryError | StalledError {\n let mut allocator = Allocator.systemAllocatorProvider()\n let queue = run Shared.make(ReadyQueue {\n head: NoLink {},\n tail: NoLink {},\n }) |> Effect.provideMut(&mut allocator)\n let identities = run Shared.make(TaskIdSource {\n next: 1,\n exhausted: false,\n }) |> Effect.provideMut(&mut allocator)\n let prepared = run Fiber.prepare()\n |> Effect.provideMut(&mut allocator)\n let rootState = run Shared.make>(RootState {\n phase: RootPending {},\n }) |> Effect.provideMut(&mut allocator)\n let response = run Shared.make(Scheduler.PublicationResponse {\n phase: Scheduler.ResponseWaiting {},\n }) |> Effect.provideMut(&mut allocator)\n let submission = run Shared.make(Scheduler.SubmissionSlot {\n phase: Scheduler.NoSubmission {},\n }) |> Effect.provideMut(&mut allocator)\n let mailboxResponse = Shared.clone(&response)\n let mailbox = run Shared.make(Scheduler.TaskMailbox {\n request: Scheduler.NoRequest {},\n response: move mailboxResponse,\n }) |> Effect.provideMut(&mut allocator)\n let root = Scheduler.TaskId { value: 0 }\n let node = run Shared.make(ReadyNode {\n identity: root,\n next: NoLink {},\n enqueued: false,\n }) |> Effect.provideMut(&mut allocator)\n let Fiber.PreparedFiber {\n producer,\n canceller,\n fiber: rootFiber,\n } = move prepared\n drop rootFiber\n let client = SchedulerClient {\n identity: root,\n mailbox: Shared.clone(&mailbox),\n submission: Shared.clone(&submission),\n response: move response,\n identities: move identities,\n queue: Shared.clone(&queue),\n }\n let endpoint = ReadyEndpoint {\n queue: Shared.clone(&queue),\n node: move node,\n }\n let execution = run Execution.make(\n runRoot(\n move program,\n move client,\n Shared.clone>(&rootState),\n move producer,\n ),\n move endpoint,\n notifyReady,\n ) |> Effect.provideMut(&mut allocator)\n let entry = TaskEntry {\n parent: NoLink {},\n firstChild: NoLink {},\n nextSibling: NoLink {},\n cancellationNext: NoLink {},\n phase: Initial {},\n execution: StoredExecution { execution: move execution },\n canceller: move canceller,\n mailbox: move mailbox,\n submission: move submission,\n }\n let mut driver = Driver {\n tasks: HashMap.make(Hash.seed(0)),\n queue: move queue,\n current: root,\n cancellationHead: NoLink {},\n rootComplete: false,\n }\n let inserted = run HashMap.insert(\n &mut driver.tasks,\n root,\n move entry,\n ) |> Effect.provideMut(&mut allocator)\n drop inserted\n let notified = HashMap.withMut(&mut driver.tasks, root, notifyInitialEntry)\n let completed = run driveUntilRoot(move driver)\n drop completed\n let mut extraction = RootExtraction { phase: RootPending {} }\n let extracted = Shared.withMut(&rootState, extractRoot(&mut extraction))\n let RootExtraction { phase: selected } = move extraction\n drop rootState\n return run finishRoot(move selected)\n}\n', }, { module: 'silk/logger', path: 'silk/logger.silk', sourceIdentity: 'silk/logger', - digest: '562f3261513609b91489df50f51c0c7b145fb374ee0b3c724f888d888b2a17a1', + digest: 'c1461739a9cc1036b957df65677162a37ab17f442196292cd6912866c4923605', documentation: 'silk/logger.silk', layer: 'portable', runtimeInventory: ['bindRequirementMut', 'effectResult', 'replace'], namespace: 'Logger', aliases: ['InMemoryLogger', 'LogError', 'LogLevel', 'StdoutLogger'], source: - '//! Typed semantic logging with replaceable stdout and bounded in-memory providers.\n//!\n//! # When to use\n//! Require [`Logger`] when code emits whole semantic messages but should not choose storage or\n//! destination. Provide [`StdoutLogger`] at a process edge. Use [`InMemoryLogger`] for\n//! deterministic observation and failure tests.\n//!\n//! # Details\n//! Each invocation carries one [`LogLevel`] and one valid UTF-8 message. The service does not add\n//! formatting, newlines, timestamps, or allocation requirements. The stdout provider forwards only\n//! the message bytes; the in-memory provider retains at most eight committed events and 64 message\n//! bytes, and exposes attempted calls separately from successful commits.\n//!\n//! # Gotchas\n//! Logger failures are typed [`LogError`] values and do not guarantee that a message committed.\n//! In-memory accessors require an event index less than [`length`] and a valid message-byte index.\n//!\n//! # Examples\n//! ## Record and inspect one warning\n//!\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! import silk.logger { Logger }\n//! import silk.logger { LogLevel }\n//!\n//! import silk.usize as usize\n//!\n//! effect fn program() -> i32\n//! ! Logger.LogError {\n//! let mut logger = Logger.inMemoryProvider()\n//! let logged = run Effect.logWarning("cache miss")\n//! |> Effect.provideMut(&mut logger)\n//! if Logger.length(&logger) != usize.ONE {\n//! return 1\n//! }\n//! if Logger.levelAt(&logger, usize.ZERO) != LogLevel.Warning {\n//! return 2\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Logger.LogError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.i32 as i32\nimport silk.result { Failure, Result, Success }\nimport silk.standard_streams {\n NativeStandardStreams,\n StreamWriteError,\n nativeStandardStreamProvider as nativeStreams,\n send\n}\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// One closed logging severity from Trace through Error.\npub enum LogLevel {\n /// Detailed diagnostic events.\n Trace,\n /// Development diagnostic events.\n Debug,\n /// Ordinary operational events.\n Info,\n /// Recoverable abnormal conditions.\n Warning,\n /// Operations that did not complete as intended.\n Error\n}\n\n/// A typed failure reported by one [`Logger`] provider.\n///\n/// # Details\n///\n/// The numeric code belongs to the provider. Portable code can recover from `LogError` without\n/// assigning one meaning to that code across different providers.\npub struct LogError {\n code: i32\n}\n\n/// Returns the provider-defined failure code for diagnostics.\n///\n/// # Gotchas\n///\n/// Interpret this code only with knowledge of the selected provider. Different providers can use\n/// the same code for different failures.\npub fn errorCode(error: LogError) -> i32 { return error.code }\n\neffect fn reject(code: i32) -> never ! LogError {\n fail LogError { code: code }\n}\n\n/// A replaceable service that receives one complete semantic log event per call.\n///\n/// # When to use\n///\n/// Use this service when library code must emit events without selecting stdout, memory, or another\n/// destination.\n///\n/// # Details\n///\n/// Each call carries one severity and one valid UTF-8 message. The service does not require a\n/// newline, timestamp, prefix, allocation, or output destination. The provider owns those choices.\npub service Logger {\n /// Submits one complete UTF-8 message at one severity to the active provider.\n ///\n /// # Details\n ///\n /// The call preserves the message bytes exactly. It does not add a newline, severity label,\n /// timestamp, or other formatting. A provider failure produces [`LogError`].\n effect fn log(\n level: LogLevel,\n message: string\n ) -> () ! LogError ? &mut Logger\n}\n\n/// A [`Logger`] provider that writes each complete message to process standard output.\n///\n/// # Details\n///\n/// The provider ignores the severity for physical formatting and writes only the UTF-8 message\n/// bytes. It adds no newline and performs no message allocation.\npub struct StdoutLogger {}\n\n/// Creates a logger that forwards each complete message to process standard output.\n///\n/// # Gotchas\n///\n/// The caller must include a newline in `message` when line separation is required. A standard-\n/// output write failure becomes a provider-defined [`LogError`].\npub fn stdoutProvider() -> StdoutLogger { return StdoutLogger {} }\n\neffect fn writeStdoutCounted(\n streams: &mut NativeStandardStreams,\n message: &[u8]\n) -> i32 ! StreamWriteError {\n let written = run Intrinsic.bindRequirementMut(send(false, message), streams)\n return 0\n}\n\neffect fn writeStdout(\n self: &mut StdoutLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let mut streams = nativeStreams()\n let bytes = stringUtf8Bytes(message)\n let completed = run Intrinsic.effectResult(writeStdoutCounted(&mut streams, bytes))\n return match move completed {\n Result { value: outcome } => match move outcome {\n Success { value: success } => ()\n Failure { error: failure } => run reject(3)\n }\n }\n}\n\nimpl Logger for StdoutLogger {\n log: StdoutLogger.writeStdout\n}\n\n/// A deterministic [`Logger`] provider that retains up to eight events and 64 total message bytes.\n///\n/// # When to use\n///\n/// Use this provider in tests that must inspect event order, severity, message bytes, or failure\n/// behavior without process output.\n///\n/// # Details\n///\n/// The provider copies each committed message into fixed internal storage. It records attempted\n/// calls separately from committed events. Capacity failure and configured failure do not commit an\n/// event.\npub struct InMemoryLogger {\n levels: [LogLevel; 8]\n offsets: [usize; 8]\n lengths: [usize; 8]\n messages: [i32; 64]\n count: usize\n messageLength: usize\n attempts: usize\n failEnabled: bool\n failAt: usize\n}\n\nfn emptyLevels() -> [LogLevel; 8] {\n return [\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace\n ]\n}\n\nfn emptyIndexes() -> [usize; 8] { return [0, 0, 0, 0, 0, 0, 0, 0] }\n\nfn emptyMessages() -> [i32; 64] {\n return [\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n ]\n}\n\n/// Creates an empty in-memory logger with capacity for eight events and 64 message bytes.\n///\n/// # Gotchas\n///\n/// A call fails when eight events are already committed. A call also fails when its bytes exceed\n/// the remaining 64-byte total. Neither failure commits the event.\npub fn inMemoryProvider() -> InMemoryLogger {\n return InMemoryLogger {\n levels: emptyLevels(),\n offsets: emptyIndexes(),\n lengths: emptyIndexes(),\n messages: emptyMessages(),\n count: usize.add(0, 0),\n messageLength: usize.add(0, 0),\n attempts: usize.add(0, 0),\n failEnabled: false,\n failAt: usize.add(0, 0),\n }\n}\n\n/// Creates an in-memory logger that rejects one zero-based attempted-call ordinal.\n///\n/// # Details\n///\n/// The configured attempt increases [`attempts`] but does not increase [`length`] or consume\n/// message capacity. Other attempts retain the eight-event and 64-byte limits of\n/// [`inMemoryProvider`].\npub fn inMemoryProviderFailAt(failAt: usize) -> InMemoryLogger {\n let mut logger = inMemoryProvider()\n logger.failEnabled = true\n logger.failAt = failAt\n return move logger\n}\n\neffect fn record(\n self: &mut InMemoryLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let values = stringUtf8Bytes(message)\n let ordinal = self.attempts\n self.attempts = self.attempts + usize.add(0, 1)\n if self.failEnabled {\n if ordinal == self.failAt { return run reject(1) }\n }\n\n if self.count == usize.add(0, 8) { return run reject(2) }\n if values.length > usize.add(0, 64) - self.messageLength { return run reject(2) }\n\n let offset = self.messageLength\n let mut messages = Intrinsic.replace(self.messages, emptyMessages())\n let mut index = usize.add(0, 0)\n while index < values.length {\n messages[offset + index] = u8.toI32(values[index])\n index = index + usize.add(0, 1)\n }\n let mut levels = Intrinsic.replace(self.levels, emptyLevels())\n let mut offsets = Intrinsic.replace(self.offsets, emptyIndexes())\n let mut lengths = Intrinsic.replace(self.lengths, emptyIndexes())\n levels[self.count] = level\n offsets[self.count] = offset\n lengths[self.count] = values.length\n self.messages = move messages\n self.levels = move levels\n self.offsets = move offsets\n self.lengths = move lengths\n self.count = self.count + usize.add(0, 1)\n self.messageLength = self.messageLength + values.length\n return ()\n}\n\nimpl Logger for InMemoryLogger {\n log: InMemoryLogger.record\n}\n\n/// Returns the number of events that the in-memory logger committed.\n///\n/// # Details\n///\n/// Failed attempts do not increase this count. Use [`attempts`] when rejected calls must also be\n/// observed.\npub fn length(self: &InMemoryLogger) -> usize {\n return self.count\n}\n\n/// Returns the severity of one committed event.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns the initial Trace\n/// value instead of trapping. An index of eight or more traps.\npub fn levelAt(self: &InMemoryLogger, index: usize) -> LogLevel {\n let levels = self.levels\n return levels[index]\n}\n\n/// Returns the UTF-8 byte length of one committed message.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns zero instead of\n/// trapping. An index of eight or more traps.\npub fn messageLengthAt(self: &InMemoryLogger, index: usize) -> usize {\n let lengths = self.lengths\n return lengths[index]\n}\n\n/// Returns one UTF-8 byte from a committed message.\n///\n/// # Gotchas\n///\n/// `eventIndex` must be less than [`length`]. `byteIndex` must be less than\n/// [`messageLengthAt`] for that event. An unused event or invalid byte index traps. An event index\n/// of eight or more also traps.\npub fn messageByteAt(\n self: &InMemoryLogger,\n eventIndex: usize,\n byteIndex: usize\n) -> u8 {\n let lengths = self.lengths\n let length = lengths[eventIndex]\n if length <= byteIndex { let boom = 1 / 0 }\n let offsets = self.offsets\n let offset = offsets[eventIndex]\n let messages = self.messages\n return i32.toU8(messages[offset + byteIndex])\n}\n\n/// Returns the number of calls attempted, including calls that produced [`LogError`].\npub fn attempts(self: &InMemoryLogger) -> usize { return self.attempts }\n', + '//! Typed semantic logging with replaceable stdout and bounded in-memory providers.\n//!\n//! # When to use\n//! Require [`Logger`] when code emits whole semantic messages but should not choose storage or\n//! destination. Provide [`StdoutLogger`] at a process edge. Use [`InMemoryLogger`] for\n//! deterministic observation and failure tests.\n//!\n//! # Details\n//! Each invocation carries one [`LogLevel`] and one valid UTF-8 message. The service does not add\n//! formatting, newlines, timestamps, or allocation requirements. The stdout provider forwards only\n//! the message bytes; the in-memory provider retains at most eight committed events and 64 message\n//! bytes, and exposes attempted calls separately from successful commits.\n//!\n//! # Gotchas\n//! Logger failures are typed [`LogError`] values and do not guarantee that a message committed.\n//! In-memory accessors require an event index less than [`length`] and a valid message-byte index.\n//!\n//! # Examples\n//! ## Record and inspect one warning\n//!\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! import silk.logger { Logger }\n//! import silk.logger { LogLevel }\n//!\n//! import silk.usize as usize\n//!\n//! effect fn program() -> i32\n//! ! Logger.LogError {\n//! let mut logger = Logger.inMemoryProvider()\n//! let logged = run Effect.logWarning("cache miss")\n//! |> Effect.provideMut(&mut logger)\n//! if Logger.length(&logger) != usize.ONE {\n//! return 1\n//! }\n//! if Logger.levelAt(&logger, usize.ZERO) != LogLevel.Warning {\n//! return 2\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Logger.LogError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.i32 as i32\nimport silk.result { Result }\nimport silk.standard_streams {\n NativeStandardStreams,\n StreamWriteError,\n nativeStandardStreamProvider as nativeStreams,\n send\n}\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// One closed logging severity from Trace through Error.\npub enum LogLevel {\n /// Detailed diagnostic events.\n Trace,\n /// Development diagnostic events.\n Debug,\n /// Ordinary operational events.\n Info,\n /// Recoverable abnormal conditions.\n Warning,\n /// Operations that did not complete as intended.\n Error\n}\n\n/// A typed failure reported by one [`Logger`] provider.\n///\n/// # Details\n///\n/// The numeric code belongs to the provider. Portable code can recover from `LogError` without\n/// assigning one meaning to that code across different providers.\npub struct LogError {\n code: i32\n}\n\n/// Returns the provider-defined failure code for diagnostics.\n///\n/// # Gotchas\n///\n/// Interpret this code only with knowledge of the selected provider. Different providers can use\n/// the same code for different failures.\npub fn errorCode(error: LogError) -> i32 { return error.code }\n\neffect fn reject(code: i32) -> never ! LogError {\n fail LogError { code: code }\n}\n\n/// A replaceable service that receives one complete semantic log event per call.\n///\n/// # When to use\n///\n/// Use this service when library code must emit events without selecting stdout, memory, or another\n/// destination.\n///\n/// # Details\n///\n/// Each call carries one severity and one valid UTF-8 message. The service does not require a\n/// newline, timestamp, prefix, allocation, or output destination. The provider owns those choices.\npub service Logger {\n /// Submits one complete UTF-8 message at one severity to the active provider.\n ///\n /// # Details\n ///\n /// The call preserves the message bytes exactly. It does not add a newline, severity label,\n /// timestamp, or other formatting. A provider failure produces [`LogError`].\n effect fn log(\n level: LogLevel,\n message: string\n ) -> () ! LogError ? &mut Logger\n}\n\n/// A [`Logger`] provider that writes each complete message to process standard output.\n///\n/// # Details\n///\n/// The provider ignores the severity for physical formatting and writes only the UTF-8 message\n/// bytes. It adds no newline and performs no message allocation.\npub struct StdoutLogger {}\n\n/// Creates a logger that forwards each complete message to process standard output.\n///\n/// # Gotchas\n///\n/// The caller must include a newline in `message` when line separation is required. A standard-\n/// output write failure becomes a provider-defined [`LogError`].\npub fn stdoutProvider() -> StdoutLogger { return StdoutLogger {} }\n\neffect fn writeStdoutCounted(\n streams: &mut NativeStandardStreams,\n message: &[u8]\n) -> i32 ! StreamWriteError {\n let written = run Intrinsic.bindRequirementMut(send(false, message), streams)\n return 0\n}\n\neffect fn writeStdout(\n self: &mut StdoutLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let mut streams = nativeStreams()\n let bytes = stringUtf8Bytes(message)\n let completed = run Intrinsic.effectResult(writeStdoutCounted(&mut streams, bytes))\n return match move completed {\n Result.Success { value: success } => ()\n Result.Failure { error: failure } => run reject(3)\n }\n}\n\nimpl Logger for StdoutLogger {\n log: StdoutLogger.writeStdout\n}\n\n/// A deterministic [`Logger`] provider that retains up to eight events and 64 total message bytes.\n///\n/// # When to use\n///\n/// Use this provider in tests that must inspect event order, severity, message bytes, or failure\n/// behavior without process output.\n///\n/// # Details\n///\n/// The provider copies each committed message into fixed internal storage. It records attempted\n/// calls separately from committed events. Capacity failure and configured failure do not commit an\n/// event.\npub struct InMemoryLogger {\n levels: [LogLevel; 8]\n offsets: [usize; 8]\n lengths: [usize; 8]\n messages: [i32; 64]\n count: usize\n messageLength: usize\n attempts: usize\n failEnabled: bool\n failAt: usize\n}\n\nfn emptyLevels() -> [LogLevel; 8] {\n return [\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace\n ]\n}\n\nfn emptyIndexes() -> [usize; 8] { return [0, 0, 0, 0, 0, 0, 0, 0] }\n\nfn emptyMessages() -> [i32; 64] {\n return [\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n ]\n}\n\n/// Creates an empty in-memory logger with capacity for eight events and 64 message bytes.\n///\n/// # Gotchas\n///\n/// A call fails when eight events are already committed. A call also fails when its bytes exceed\n/// the remaining 64-byte total. Neither failure commits the event.\npub fn inMemoryProvider() -> InMemoryLogger {\n return InMemoryLogger {\n levels: emptyLevels(),\n offsets: emptyIndexes(),\n lengths: emptyIndexes(),\n messages: emptyMessages(),\n count: usize.add(0, 0),\n messageLength: usize.add(0, 0),\n attempts: usize.add(0, 0),\n failEnabled: false,\n failAt: usize.add(0, 0),\n }\n}\n\n/// Creates an in-memory logger that rejects one zero-based attempted-call ordinal.\n///\n/// # Details\n///\n/// The configured attempt increases [`attempts`] but does not increase [`length`] or consume\n/// message capacity. Other attempts retain the eight-event and 64-byte limits of\n/// [`inMemoryProvider`].\npub fn inMemoryProviderFailAt(failAt: usize) -> InMemoryLogger {\n let mut logger = inMemoryProvider()\n logger.failEnabled = true\n logger.failAt = failAt\n return move logger\n}\n\neffect fn record(\n self: &mut InMemoryLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let values = stringUtf8Bytes(message)\n let ordinal = self.attempts\n self.attempts = self.attempts + usize.add(0, 1)\n if self.failEnabled {\n if ordinal == self.failAt { return run reject(1) }\n }\n\n if self.count == usize.add(0, 8) { return run reject(2) }\n if values.length > usize.add(0, 64) - self.messageLength { return run reject(2) }\n\n let offset = self.messageLength\n let mut messages = Intrinsic.replace(self.messages, emptyMessages())\n let mut index = usize.add(0, 0)\n while index < values.length {\n messages[offset + index] = u8.toI32(values[index])\n index = index + usize.add(0, 1)\n }\n let mut levels = Intrinsic.replace(self.levels, emptyLevels())\n let mut offsets = Intrinsic.replace(self.offsets, emptyIndexes())\n let mut lengths = Intrinsic.replace(self.lengths, emptyIndexes())\n levels[self.count] = level\n offsets[self.count] = offset\n lengths[self.count] = values.length\n self.messages = move messages\n self.levels = move levels\n self.offsets = move offsets\n self.lengths = move lengths\n self.count = self.count + usize.add(0, 1)\n self.messageLength = self.messageLength + values.length\n return ()\n}\n\nimpl Logger for InMemoryLogger {\n log: InMemoryLogger.record\n}\n\n/// Returns the number of events that the in-memory logger committed.\n///\n/// # Details\n///\n/// Failed attempts do not increase this count. Use [`attempts`] when rejected calls must also be\n/// observed.\npub fn length(self: &InMemoryLogger) -> usize {\n return self.count\n}\n\n/// Returns the severity of one committed event.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns the initial Trace\n/// value instead of trapping. An index of eight or more traps.\npub fn levelAt(self: &InMemoryLogger, index: usize) -> LogLevel {\n let levels = self.levels\n return levels[index]\n}\n\n/// Returns the UTF-8 byte length of one committed message.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns zero instead of\n/// trapping. An index of eight or more traps.\npub fn messageLengthAt(self: &InMemoryLogger, index: usize) -> usize {\n let lengths = self.lengths\n return lengths[index]\n}\n\n/// Returns one UTF-8 byte from a committed message.\n///\n/// # Gotchas\n///\n/// `eventIndex` must be less than [`length`]. `byteIndex` must be less than\n/// [`messageLengthAt`] for that event. An unused event or invalid byte index traps. An event index\n/// of eight or more also traps.\npub fn messageByteAt(\n self: &InMemoryLogger,\n eventIndex: usize,\n byteIndex: usize\n) -> u8 {\n let lengths = self.lengths\n let length = lengths[eventIndex]\n if length <= byteIndex { let boom = 1 / 0 }\n let offsets = self.offsets\n let offset = offsets[eventIndex]\n let messages = self.messages\n return i32.toU8(messages[offset + byteIndex])\n}\n\n/// Returns the number of calls attempted, including calls that produced [`LogError`].\npub fn attempts(self: &InMemoryLogger) -> usize { return self.attempts }\n', }, { module: 'silk/metrics', @@ -793,14 +793,14 @@ export const modules = [ module: 'silk/option', path: 'silk/option.silk', sourceIdentity: 'silk/option', - digest: '3dc98fb83d62c0ab4ecfcbd9fac595cbcd9fa84119360430311cc16620c8725a', + digest: '941cd279b6e978566b65cbf447ec86bd92bce867cb0d2746b4b79449e6e46a21', documentation: 'silk/option.silk', layer: 'portable', runtimeInventory: [], namespace: 'Option', aliases: ['None', 'Some'], source: - "//! Optional owned values that distinguish presence from absence without a failure channel.\n//!\n//! # When to use\n//! Use [`Option`] when absence is an expected answer and needs no error payload. Use [`map`] for a\n//! pure transform, [`flatMap`] when the transform may also return absence, and [`unwrapOr`] only\n//! when the caller is ready to consume the option.\n//!\n//! # Details\n//! `Option` is the structural union of [`Some`] and [`None`]. Its combinators preserve affine\n//! ownership: a present value moves forward, while an unused fallback or abandoned branch drops.\n//!\n//! # Examples\n//! ## Transform and continue only when a value is present\n//! ```silk\n//! import silk.option as Option\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! fn positive(value: i32) -> Option.Option {\n//! if value > 0 {\n//! return Option.some(value)\n//! }\n//! return Option.none()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Option.some(21)\n//! let doubled = Option.map(move initial, double)\n//! let answer = Option.flatMap(move doubled, positive)\n//! let absent = Option.none()\n//! let missing = Option.map(move absent, double)\n//! let presentValue = Option.unwrapOr(move answer, 0)\n//! let absentValue = Option.unwrapOr(move missing, 0)\n//! return presentValue + absentValue\n//! }\n//! ```\n\n// Canonical recoverable outcome members. The compiler's Option is represented transparently by the\n// structural union Some | None; the named declaration is its source-navigation anchor.\n\n\n\n/// The present member of [`Option`], carrying the available owned value.\npub struct Some {\n /// The value moved through present-only combinator branches.\n value: T\n}\n\n/// The absent member of [`Option`]; it carries no explanation for the absence.\npub struct None {}\n\n/// An owned value that is either [`Some`] or [`None`].\n///\n/// # Details\n///\n/// Match on an `Option` when both arms need custom behavior. Prefer [`map`], [`flatMap`], or\n/// [`unwrapOr`] for the common transform, continue, and default cases.\npub struct Option {\n /// The structural outcome narrowed by `match`.\n value: Some | None\n}\n\n/// Constructs an absent optional value of the requested element type.\npub fn none() -> Option {\n return None {}\n}\n\n/// Constructs a present option by moving `value` into it.\npub fn some(value: T) -> Option {\n return Some { value: move value }\n}\n\n/// Applies `transform` once to a present value and keeps an absent value absent.\n///\n/// # Details\n///\n/// The callback is not called for [`None`]. This operation consumes `self`; use a shared borrow and\n/// `match` instead when the original option must remain available.\npub fn map(self: Option, transform: once fn(T) -> U) -> Option {\n return match move self {\n Some { value } => some(transform(move value))\n None {} => none()\n }\n}\n\n/// Continues a present value with a transform that itself answers with an Option, so the\n/// outcome stays one Option deep instead of nesting.\n///\n/// # Details\n///\n/// The callback runs once for [`Some`] and not at all for [`None`]. Use this when the next step may\n/// reject the value without needing to explain why; use a `Result` when rejection needs an error.\npub fn flatMap(self: Option, transform: once fn(T) -> Option) -> Option {\n return match move self {\n Some { value } => transform(move value)\n None {} => none()\n }\n}\n\n/// Returns the present value, or the fallback value when the option is absent.\n///\n/// # Details\n///\n/// Only the absent arm consumes the fallback. The present arm releases it, so exactly one of the\n/// two owned values leaves this call and the other drops.\n///\n/// # Examples\n///\n/// ## Choose between a present value and a fallback\n///\n/// ```silk\n/// import silk.option as Option\n///\n/// pub fn main() -> i32 {\n/// let present = Option.some(7)\n/// let absent = Option.none()\n/// let first = move present\n/// |> Option.unwrapOr(0)\n/// let second = move absent\n/// |> Option.unwrapOr(5)\n/// return first + second\n/// }\n/// ```\npub fn unwrapOr(\n self: Option,\n /// The owned alternative consumed only when `self` is absent.\n fallback: T,\n) -> T {\n return match move self {\n Some { value } => keepPresent(move value, move fallback)\n None {} => move fallback\n }\n}\n\n/// Releases the fallback that a present value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepPresent(present: T, unused: T) -> T {\n drop unused\n return move present\n}\n", + '//! Optional owned values that distinguish presence from absence without a failure channel.\n//!\n//! # When to use\n//! Use [`Option`] when absence is an expected answer and needs no error payload. Use [`map`] for a\n//! pure transform, [`flatMap`] when the transform may also return absence, and [`unwrapOr`] only\n//! when the caller is ready to consume the option.\n//!\n//! # Details\n//! `Option` is a nominal union with `Some` and `None` variants. Its combinators preserve affine\n//! ownership: a present value moves forward, while an unused fallback or abandoned branch drops.\n//!\n//! # Examples\n//! ## Transform and continue only when a value is present\n//! ```silk\n//! import silk.option as Option\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! fn positive(value: i32) -> Option.Option {\n//! if value > 0 {\n//! return Option.some(value)\n//! }\n//! return Option.none()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Option.some(21)\n//! let doubled = Option.map(move initial, double)\n//! let answer = Option.flatMap(move doubled, positive)\n//! let absent = Option.none()\n//! let missing = Option.map(move absent, double)\n//! let presentValue = Option.unwrapOr(move answer, 0)\n//! let absentValue = Option.unwrapOr(move missing, 0)\n//! return presentValue + absentValue\n//! }\n//! ```\n\n/// An owned value that is either [`Some`] or [`None`].\n///\n/// # Details\n///\n/// Match on an `Option` when both arms need custom behavior. Prefer [`map`], [`flatMap`], or\n/// [`unwrapOr`] for the common transform, continue, and default cases.\npub union Option {\n /// The absent variant; it carries no explanation for the absence.\n None,\n /// The present variant, carrying the available owned value.\n Some {\n /// The value moved through present-only combinator branches.\n value: T\n }\n}\n\n/// Constructs an absent optional value of the requested element type.\npub fn none() -> Option {\n return Option.None\n}\n\n/// Constructs a present option by moving `value` into it.\npub fn some(value: T) -> Option {\n return Option.Some { value: move value }\n}\n\n/// Applies `transform` once to a present value and keeps an absent value absent.\n///\n/// # Details\n///\n/// The callback is not called for [`None`]. This operation consumes `self`; use a shared borrow and\n/// `match` instead when the original option must remain available.\npub fn map(self: Option, transform: once fn(T) -> U) -> Option {\n return match move self {\n Option.Some { value } => some(transform(move value))\n Option.None => none()\n }\n}\n\n/// Continues a present value with a transform that itself answers with an Option, so the\n/// outcome stays one Option deep instead of nesting.\n///\n/// # Details\n///\n/// The callback runs once for [`Some`] and not at all for [`None`]. Use this when the next step may\n/// reject the value without needing to explain why; use a `Result` when rejection needs an error.\npub fn flatMap(self: Option, transform: once fn(T) -> Option) -> Option {\n return match move self {\n Option.Some { value } => transform(move value)\n Option.None => none()\n }\n}\n\n/// Returns the present value, or the fallback value when the option is absent.\n///\n/// # Details\n///\n/// Only the absent arm consumes the fallback. The present arm releases it, so exactly one of the\n/// two owned values leaves this call and the other drops.\n///\n/// # Examples\n///\n/// ## Choose between a present value and a fallback\n///\n/// ```silk\n/// import silk.option as Option\n///\n/// pub fn main() -> i32 {\n/// let present = Option.some(7)\n/// let absent = Option.none()\n/// let first = move present\n/// |> Option.unwrapOr(0)\n/// let second = move absent\n/// |> Option.unwrapOr(5)\n/// return first + second\n/// }\n/// ```\npub fn unwrapOr(\n self: Option,\n /// The owned alternative consumed only when `self` is absent.\n fallback: T,\n) -> T {\n return match move self {\n Option.Some { value } => keepPresent(move value, move fallback)\n Option.None => move fallback\n }\n}\n\n/// Releases the fallback that a present value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepPresent(present: T, unused: T) -> T {\n drop unused\n return move present\n}\n', }, { module: 'silk/order', @@ -830,20 +830,20 @@ export const modules = [ module: 'silk/os_child_process', path: 'silk/os_child_process.silk', sourceIdentity: 'silk/os_child_process', - digest: 'f085f3818b67707fba5aba09ccfb9295540586800223567747581f355f98e5c7', + digest: 'ed1e0a678f9724f34820e697b9eb95ab3154d223e03192089228e46e3c868c2e', documentation: 'silk/os_child_process.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], runtimeInventory: ['osProcessCapture', 'osProcessExecute'], namespace: 'OsChildProcess', source: - '//! Native [`ChildProcess`] provider that executes directly through the platform process boundary.\n//!\n//! # When to use\n//! Construct [`OsChildProcess`] at a native application edge, then provide it to portable code that\n//! requires [`ChildProcess`]. Tests can supply an in-memory provider without importing this module.\n//!\n//! # Details\n//! The provider owns no persistent state. It translates low-level spawn and capture reasons into\n//! portable [`ProcessError`] data, preserves a native numeric code, and copies the complete stdout\n//! and stderr captures into independently owned [`Bytes`] values. Exit status and signal\n//! termination remain ordinary [`ProcessOutcome`] data.\n//!\n//! Constructing the provider performs no process operation. Portable code invokes\n//! `ChildProcess.execute` after an application provides `&mut OsChildProcess` for the\n//! `&mut ChildProcess` requirement and supplies an allocator for owned captures.\n//!\n//! # Gotchas\n//! Reachable OS process operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing a process host import; evaluator execution requires an injected host\n//! adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without starting a process\n//!\n//! ```silk\n//! import silk.os_child_process as OsChildProcess\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsChildProcess.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.child_process {\n ChildProcess,\n ProcessError,\n ProcessOperation,\n ProcessOutcome,\n ProcessReason,\n ProcessRequest,\n arguments as requestArguments,\n captureOperation,\n environment as requestEnvironment,\n exited,\n failureWithCode,\n invalidRequest,\n noSpace,\n notFound,\n other,\n permissionDenied,\n program as requestProgram,\n signaled,\n spawnOperation,\n unsupported,\n workingDirectory as requestWorkingDirectory\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.i32 as i32\nimport silk.option { None, Option, Some, none }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`ChildProcess`] provider with blocking execution and complete capture.\n///\n/// # Details\n///\n/// The provider borrows the request and transfers each completed capture into independent [`Bytes`]\n/// storage. The returned [`ProcessOutcome`] owns that storage.\npub struct OsChildProcess {}\n\n/// Creates a stateless provider for the native process boundary.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut ChildProcess` to\n/// portable code that calls `ChildProcess.execute` or `silk.child_process.submit`.\n///\n/// # Details\n///\n/// Construction starts no process and allocates no storage. Each execution translates native\n/// failures into [`ProcessError`] and requires an allocator for owned output captures.\npub fn make() -> OsChildProcess {\n return OsChildProcess {}\n}\n\nfn reason(value: i32) -> ProcessReason {\n if value == 0 { return notFound() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidRequest() }\n if value == 4 { return invalidRequest() }\n if value == 6 { return noSpace() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(\n operation: ProcessOperation,\n lowReason: i32,\n nativeCode: u32\n) -> never ! ProcessError {\n fail failureWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawExecute(\n program: &[u8],\n arguments: &[u8],\n environment: &[u8],\n workingDirectory: &[u8],\n status: &mut i32,\n code: &mut i32,\n outputLength: &mut usize,\n errorLength: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osProcessExecute(\n program,\n arguments,\n environment,\n workingDirectory,\n status,\n code,\n outputLength,\n errorLength,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\neffect fn rawCapture(\n stream: i32,\n offset: usize,\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osProcessCapture(stream, offset, output, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Copies one completed capture out of the boundary into independently owned bytes.\n///\n/// The preceding execute reported the exact length, so the outcome owns every captured byte or the\n/// capture stage fails; a short transfer that never advances is a capture failure rather than a\n/// silently truncated result.\neffect fn drain(stream: i32, length: usize) -> Bytes ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n if length == usize.ZERO { return move result }\n let mut buffer = run bytesZeroed(length)\n let mut offset = usize.ZERO\n while offset < length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let transferred = run rawCapture(stream, offset, move output, &mut lowReason, &mut nativeCode)\n let received = match move transferred {\n None {} => run raise(captureOperation(), lowReason, nativeCode)\n Some { value: selected } => selected\n }\n if received == usize.ZERO { return run raise(captureOperation(), 10, u32.toU32(0)) }\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < received {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n offset = offset + received\n }\n return move result\n}\n\n/// Runs one request to completion and owns everything the child wrote.\n///\n/// The boundary reports termination as a status selector plus a code, which becomes `Exited` or\n/// `Signaled` here. A nonzero exit code is neither: it is data inside `Exited`.\neffect fn execute(\n self: &mut OsChildProcess,\n request: &ProcessRequest\n) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut status = 0\n let mut code = 0\n let mut outputLength = usize.ZERO\n let mut errorLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let started = run rawExecute(\n requestProgram(request),\n requestArguments(request),\n requestEnvironment(request),\n requestWorkingDirectory(request),\n &mut status,\n &mut code,\n &mut outputLength,\n &mut errorLength,\n &mut lowReason,\n &mut nativeCode\n )\n if started == false { return run raise(spawnOperation(), lowReason, nativeCode) }\n let output = run drain(0, outputLength)\n let errors = run drain(1, errorLength)\n if status == 0 { return exited(code, move output, move errors) }\n return signaled(code, move output, move errors)\n}\n\nimpl ChildProcess for OsChildProcess {\n execute: OsChildProcess.execute\n}\n', + '//! Native [`ChildProcess`] provider that executes directly through the platform process boundary.\n//!\n//! # When to use\n//! Construct [`OsChildProcess`] at a native application edge, then provide it to portable code that\n//! requires [`ChildProcess`]. Tests can supply an in-memory provider without importing this module.\n//!\n//! # Details\n//! The provider owns no persistent state. It translates low-level spawn and capture reasons into\n//! portable [`ProcessError`] data, preserves a native numeric code, and copies the complete stdout\n//! and stderr captures into independently owned [`Bytes`] values. Exit status and signal\n//! termination remain ordinary [`ProcessOutcome`] data.\n//!\n//! Constructing the provider performs no process operation. Portable code invokes\n//! `ChildProcess.execute` after an application provides `&mut OsChildProcess` for the\n//! `&mut ChildProcess` requirement and supplies an allocator for owned captures.\n//!\n//! # Gotchas\n//! Reachable OS process operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing a process host import; evaluator execution requires an injected host\n//! adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without starting a process\n//!\n//! ```silk\n//! import silk.os_child_process as OsChildProcess\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsChildProcess.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.child_process {\n ChildProcess,\n ProcessError,\n ProcessOperation,\n ProcessOutcome,\n ProcessReason,\n ProcessRequest,\n arguments as requestArguments,\n captureOperation,\n environment as requestEnvironment,\n exited,\n failureWithCode,\n invalidRequest,\n noSpace,\n notFound,\n other,\n permissionDenied,\n program as requestProgram,\n signaled,\n spawnOperation,\n unsupported,\n workingDirectory as requestWorkingDirectory\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.i32 as i32\nimport silk.option { Option, none }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`ChildProcess`] provider with blocking execution and complete capture.\n///\n/// # Details\n///\n/// The provider borrows the request and transfers each completed capture into independent [`Bytes`]\n/// storage. The returned [`ProcessOutcome`] owns that storage.\npub struct OsChildProcess {}\n\n/// Creates a stateless provider for the native process boundary.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut ChildProcess` to\n/// portable code that calls `ChildProcess.execute` or `silk.child_process.submit`.\n///\n/// # Details\n///\n/// Construction starts no process and allocates no storage. Each execution translates native\n/// failures into [`ProcessError`] and requires an allocator for owned output captures.\npub fn make() -> OsChildProcess {\n return OsChildProcess {}\n}\n\nfn reason(value: i32) -> ProcessReason {\n if value == 0 { return notFound() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidRequest() }\n if value == 4 { return invalidRequest() }\n if value == 6 { return noSpace() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(\n operation: ProcessOperation,\n lowReason: i32,\n nativeCode: u32\n) -> never ! ProcessError {\n fail failureWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawExecute(\n program: &[u8],\n arguments: &[u8],\n environment: &[u8],\n workingDirectory: &[u8],\n status: &mut i32,\n code: &mut i32,\n outputLength: &mut usize,\n errorLength: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osProcessExecute(\n program,\n arguments,\n environment,\n workingDirectory,\n status,\n code,\n outputLength,\n errorLength,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\neffect fn rawCapture(\n stream: i32,\n offset: usize,\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osProcessCapture(stream, offset, output, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Copies one completed capture out of the boundary into independently owned bytes.\n///\n/// The preceding execute reported the exact length, so the outcome owns every captured byte or the\n/// capture stage fails; a short transfer that never advances is a capture failure rather than a\n/// silently truncated result.\neffect fn drain(stream: i32, length: usize) -> Bytes ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n if length == usize.ZERO { return move result }\n let mut buffer = run bytesZeroed(length)\n let mut offset = usize.ZERO\n while offset < length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let transferred = run rawCapture(stream, offset, move output, &mut lowReason, &mut nativeCode)\n let received = match move transferred {\n Option.None => run raise(captureOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if received == usize.ZERO { return run raise(captureOperation(), 10, u32.toU32(0)) }\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < received {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n offset = offset + received\n }\n return move result\n}\n\n/// Runs one request to completion and owns everything the child wrote.\n///\n/// The boundary reports termination as a status selector plus a code, which becomes `Exited` or\n/// `Signaled` here. A nonzero exit code is neither: it is data inside `Exited`.\neffect fn execute(\n self: &mut OsChildProcess,\n request: &ProcessRequest\n) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut status = 0\n let mut code = 0\n let mut outputLength = usize.ZERO\n let mut errorLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let started = run rawExecute(\n requestProgram(request),\n requestArguments(request),\n requestEnvironment(request),\n requestWorkingDirectory(request),\n &mut status,\n &mut code,\n &mut outputLength,\n &mut errorLength,\n &mut lowReason,\n &mut nativeCode\n )\n if started == false { return run raise(spawnOperation(), lowReason, nativeCode) }\n let output = run drain(0, outputLength)\n let errors = run drain(1, errorLength)\n if status == 0 { return exited(code, move output, move errors) }\n return signaled(code, move output, move errors)\n}\n\nimpl ChildProcess for OsChildProcess {\n execute: OsChildProcess.execute\n}\n', }, { module: 'silk/os_filesystem', path: 'silk/os_filesystem.silk', sourceIdentity: 'silk/os_filesystem', - digest: '3501c3782054223f8c5decff84b96d8b29eb1bee91da62760a9c548c494c5978', + digest: '6a7645105b42d3cd27e90902d858119ed9f6fdd538e0fce9d071e9ab31055121', documentation: 'silk/os_filesystem.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], @@ -863,13 +863,13 @@ export const modules = [ ], namespace: 'OsFileSystem', source: - '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { None, Option, Some, none, some }\nimport silk.result { Failure, Result, Success }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Some { value: handle } => move handle\n None {} => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Some { value: handle } => move handle\n None {} => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError> { value: outcome } => match move outcome {\n Success<()> { value: completed } => ()\n Failure { error: failure } => ()\n }\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut received = none()\n unsafe {\n received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode)\n }\n let length = match move received {\n None {} => run raise(readFileOperation(), lowReason, nativeCode)\n Some { value: selected } => selected\n }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Intrinsic.effectResult(readLoop(&mut handle))\n let closed = run Intrinsic.effectResult(close(move handle, readFileOperation()))\n return match move attempted {\n Result { value: outcome } => match move outcome {\n Success { value: bytes } => match move closed {\n Result<(), FileError> { value: closeOutcome } => match move closeOutcome {\n Success<()> { value: completed } => move bytes\n Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n }\n Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut written = none()\n unsafe {\n written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode)\n }\n let length = match move written {\n None {} => run raise(writeFileOperation(), lowReason, nativeCode)\n Some { value: selected } => selected\n }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Intrinsic.effectResult(writeLoop(&mut handle, bytes))\n let closed = run Intrinsic.effectResult(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError> { value: outcome } => match move outcome {\n Success<()> { value: completed } => match move closed {\n Result<(), FileError> { value: closeOutcome } => match move closeOutcome {\n Success<()> { value: closedValue } => ()\n Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n }\n Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut next = none()\n unsafe {\n next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode)\n }\n let encodedLength = match move next {\n Some { value: presentLength } => presentLength + usize.ONE\n None {} => usize.ZERO\n }\n if encodedLength == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let length = encodedLength - usize.ONE\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Intrinsic.effectResult(listLoop(&mut handle, path))\n let closed = run Intrinsic.effectResult(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError> { value: outcome } => match move outcome {\n Success> { value: entries } => match move closed {\n Result<(), FileError> { value: closeOutcome } => match move closeOutcome {\n Success<()> { value: completed } => move entries\n Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n }\n Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, output, required, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n let length = match move chosen {\n Some { value: selected } => selected\n None {} => usize.ZERO\n }\n if length == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Some { value: path } => move path\n None {} => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', + '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut received = none()\n unsafe {\n received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode)\n }\n let length = match move received {\n Option.None => run raise(readFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Intrinsic.effectResult(readLoop(&mut handle))\n let closed = run Intrinsic.effectResult(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut written = none()\n unsafe {\n written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode)\n }\n let length = match move written {\n Option.None => run raise(writeFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Intrinsic.effectResult(writeLoop(&mut handle, bytes))\n let closed = run Intrinsic.effectResult(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut next = none()\n unsafe {\n next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode)\n }\n let encodedLength = match move next {\n Option.Some { value: presentLength } => presentLength + usize.ONE\n Option.None => usize.ZERO\n }\n if encodedLength == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let length = encodedLength - usize.ONE\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Intrinsic.effectResult(listLoop(&mut handle, path))\n let closed = run Intrinsic.effectResult(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, output, required, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n let length = match move chosen {\n Option.Some { value: selected } => selected\n Option.None => usize.ZERO\n }\n if length == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', }, { module: 'silk/os_host_input', path: 'silk/os_host_input.silk', sourceIdentity: 'silk/os_host_input', - digest: '923d6c4e77829f5af9509a6c57706fc7895aad2532f3b829afcb5c3d204bd4ec', + digest: '97893ddafd1e7a8575a67c8eebe8acb4923db595918153e2d85558bcac46c2aa', documentation: 'silk/os_host_input.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], @@ -881,13 +881,13 @@ export const modules = [ ], namespace: 'OsHostInput', source: - '//! Native [`HostInput`] provider for the process command line, environment, and working directory.\n//!\n//! # When to use\n//! Construct [`OsHostInput`] at a native application edge and provide it to portable code requiring\n//! [`HostInput`]. Tests can replace it with a deterministic provider and keep process state out of\n//! the program under test.\n//!\n//! # Details\n//! The provider owns no persistent state. Each successful lookup copies the host value into\n//! independent [`Bytes`], beginning with a bounded buffer and retrying once at the exact size the\n//! boundary reports. An absent argument or variable remains [`None`]; an unavailable working\n//! directory and contradictory host lengths become [`HostInputError`].\n//!\n//! Constructing the provider reads no host state. Portable code performs lookups after the\n//! application supplies `&mut OsHostInput` for the `&mut HostInput` requirement. Each owned result\n//! also requires an allocator.\n//!\n//! # Gotchas\n//! Reachable OS host-input operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing process-global imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without reading process state\n//!\n//! ```silk\n//! import silk.os_host_input as OsHostInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsHostInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n length as bytesLength,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.host_input { HostInput, HostInputError, inputFailure }\nimport silk.i32 as i32\nimport silk.option { None, Option, Some, none, some }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`HostInput`] provider for process arguments, environment, and directory.\n///\n/// # Details\n///\n/// The process owns the source values. Each successful byte lookup returns a new owned copy through\n/// the portable service.\npub struct OsHostInput {}\n\n/// Creates a stateless provider for native process input.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut HostInput` to\n/// portable lookup operations in `silk.host_input`.\n///\n/// # Details\n///\n/// Construction performs no lookup and cannot fail. Argument and environment absence remain\n/// ordinary `None` values when a later lookup runs.\npub fn make() -> OsHostInput {\n return OsHostInput {}\n}\n\neffect fn raise() -> never ! HostInputError {\n fail inputFailure()\n}\n\neffect fn rawArgumentCount(count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHostArgumentCount(count, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawArgument(\n index: usize,\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostArgument(index, output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawVariable(\n name: &[u8],\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostVariable(name, output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawWorkingDirectory(\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostWorkingDirectory(output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn ownedPrefix(view: &[u8], length: usize) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\n/// Copies one host value into independently owned bytes, growing the buffer once when needed.\n///\n/// The low-level boundary reports the value\'s complete byte length even when the buffer it received\n/// was too small, so a second pass with an exactly sized buffer always completes. An absent value\n/// is `None` with the not-found reason; any other reason is a host error.\neffect fn fetch(\n selector: i32,\n index: usize,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let initial = bytesZeroed(128)\n let mut buffer = run initial\n let mut result = none()\n let mut complete = false\n let mut grown = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut received = none()\n if selector == 0 {\n received = run rawArgument(index, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n received = run rawVariable(name, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n } else {\n received = run rawWorkingDirectory(bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n }\n }\n let encoded = match move received {\n Some { value: total } => total + usize.ONE\n None {} => usize.ZERO\n }\n if encoded == usize.ZERO {\n if lowReason != 0 { return run raise() }\n complete = true\n } else {\n let total = encoded - usize.ONE\n if total <= bytesLength(&buffer) {\n let owned = run ownedPrefix(bytesSlice(&buffer), total)\n result = some(move owned)\n complete = true\n } else {\n // An exactly sized buffer completes an honest host in one more pass, so a second short\n // answer means the host contradicted the length it just reported.\n if grown { return run raise() }\n grown = true\n let resized = bytesZeroed(total)\n buffer = run resized\n }\n }\n }\n return move result\n}\n\n/// Reports how many arguments the process received, including the program name at index zero.\neffect fn argumentCount(self: &mut OsHostInput) -> usize ! HostInputError {\n let mut count = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let answered = run rawArgumentCount(&mut count, &mut lowReason, &mut nativeCode)\n if answered == false { return run raise() }\n return count\n}\n\n/// Copies one argument\'s raw bytes, exactly as the process received them.\neffect fn argument(\n self: &mut OsHostInput,\n index: usize\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(0, index, stringUtf8Bytes(""))\n}\n\n/// Copies one environment value\'s raw bytes. An unset name is `None` rather than a failure.\neffect fn variable(\n self: &mut OsHostInput,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(1, usize.ZERO, name)\n}\n\n/// Copies the process working directory\'s raw bytes. It always exists, so absence is a host error.\neffect fn workingDirectory(\n self: &mut OsHostInput\n) -> Bytes ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let found = run fetch(2, usize.ZERO, stringUtf8Bytes(""))\n return match move found {\n Some { value: bytes } => move bytes\n None {} => run raise()\n }\n}\n\nimpl HostInput for OsHostInput {\n argumentCount: OsHostInput.argumentCount\n argument: OsHostInput.argument\n variable: OsHostInput.variable\n workingDirectory: OsHostInput.workingDirectory\n}\n', + '//! Native [`HostInput`] provider for the process command line, environment, and working directory.\n//!\n//! # When to use\n//! Construct [`OsHostInput`] at a native application edge and provide it to portable code requiring\n//! [`HostInput`]. Tests can replace it with a deterministic provider and keep process state out of\n//! the program under test.\n//!\n//! # Details\n//! The provider owns no persistent state. Each successful lookup copies the host value into\n//! independent [`Bytes`], beginning with a bounded buffer and retrying once at the exact size the\n//! boundary reports. An absent argument or variable remains [`None`]; an unavailable working\n//! directory and contradictory host lengths become [`HostInputError`].\n//!\n//! Constructing the provider reads no host state. Portable code performs lookups after the\n//! application supplies `&mut OsHostInput` for the `&mut HostInput` requirement. Each owned result\n//! also requires an allocator.\n//!\n//! # Gotchas\n//! Reachable OS host-input operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing process-global imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without reading process state\n//!\n//! ```silk\n//! import silk.os_host_input as OsHostInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsHostInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n length as bytesLength,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.host_input { HostInput, HostInputError, inputFailure }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`HostInput`] provider for process arguments, environment, and directory.\n///\n/// # Details\n///\n/// The process owns the source values. Each successful byte lookup returns a new owned copy through\n/// the portable service.\npub struct OsHostInput {}\n\n/// Creates a stateless provider for native process input.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut HostInput` to\n/// portable lookup operations in `silk.host_input`.\n///\n/// # Details\n///\n/// Construction performs no lookup and cannot fail. Argument and environment absence remain\n/// ordinary `None` values when a later lookup runs.\npub fn make() -> OsHostInput {\n return OsHostInput {}\n}\n\neffect fn raise() -> never ! HostInputError {\n fail inputFailure()\n}\n\neffect fn rawArgumentCount(count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHostArgumentCount(count, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawArgument(\n index: usize,\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostArgument(index, output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawVariable(\n name: &[u8],\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostVariable(name, output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawWorkingDirectory(\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostWorkingDirectory(output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn ownedPrefix(view: &[u8], length: usize) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\n/// Copies one host value into independently owned bytes, growing the buffer once when needed.\n///\n/// The low-level boundary reports the value\'s complete byte length even when the buffer it received\n/// was too small, so a second pass with an exactly sized buffer always completes. An absent value\n/// is `None` with the not-found reason; any other reason is a host error.\neffect fn fetch(\n selector: i32,\n index: usize,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let initial = bytesZeroed(128)\n let mut buffer = run initial\n let mut result = none()\n let mut complete = false\n let mut grown = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut received = none()\n if selector == 0 {\n received = run rawArgument(index, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n received = run rawVariable(name, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n } else {\n received = run rawWorkingDirectory(bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n }\n }\n let encoded = match move received {\n Option.Some { value: total } => total + usize.ONE\n Option.None => usize.ZERO\n }\n if encoded == usize.ZERO {\n if lowReason != 0 { return run raise() }\n complete = true\n } else {\n let total = encoded - usize.ONE\n if total <= bytesLength(&buffer) {\n let owned = run ownedPrefix(bytesSlice(&buffer), total)\n result = some(move owned)\n complete = true\n } else {\n // An exactly sized buffer completes an honest host in one more pass, so a second short\n // answer means the host contradicted the length it just reported.\n if grown { return run raise() }\n grown = true\n let resized = bytesZeroed(total)\n buffer = run resized\n }\n }\n }\n return move result\n}\n\n/// Reports how many arguments the process received, including the program name at index zero.\neffect fn argumentCount(self: &mut OsHostInput) -> usize ! HostInputError {\n let mut count = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let answered = run rawArgumentCount(&mut count, &mut lowReason, &mut nativeCode)\n if answered == false { return run raise() }\n return count\n}\n\n/// Copies one argument\'s raw bytes, exactly as the process received them.\neffect fn argument(\n self: &mut OsHostInput,\n index: usize\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(0, index, stringUtf8Bytes(""))\n}\n\n/// Copies one environment value\'s raw bytes. An unset name is `None` rather than a failure.\neffect fn variable(\n self: &mut OsHostInput,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(1, usize.ZERO, name)\n}\n\n/// Copies the process working directory\'s raw bytes. It always exists, so absence is a host error.\neffect fn workingDirectory(\n self: &mut OsHostInput\n) -> Bytes ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let found = run fetch(2, usize.ZERO, stringUtf8Bytes(""))\n return match move found {\n Option.Some { value: bytes } => move bytes\n Option.None => run raise()\n }\n}\n\nimpl HostInput for OsHostInput {\n argumentCount: OsHostInput.argumentCount\n argument: OsHostInput.argument\n variable: OsHostInput.variable\n workingDirectory: OsHostInput.workingDirectory\n}\n', }, { module: 'silk/os_monotonic_clock', path: 'silk/os_monotonic_clock.silk', sourceIdentity: 'silk/os_monotonic_clock', - digest: 'ba0788bd439729a0691145adfd12f61d124ed949c23f64749b355df1a8c60372', + digest: 'b97a5ac1e8ffd475361e7c0e49042ac153527354cdce1f9b1c1b0c015b83bdcb', documentation: 'silk/os_monotonic_clock.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], @@ -898,7 +898,7 @@ export const modules = [ ], namespace: 'OsMonotonicClock', source: - "//! Native monotonic clock provider with source-derived relative waiting.\n//!\n//! # When to use\n//! Construct [`OsMonotonicClock`] at a native application edge for elapsed-time measurement and\n//! blocking deadlines. Use a scripted provider when tests need virtual advancement.\n//!\n//! # Details\n//! Construction performs no host call. Reads, resolution queries, and absolute waits use three\n//! matching native intrinsics. `waitFor` performs exactly one starting read and uses checked\n//! split-field deadline arithmetic. It then uses the same absolute-wait operation as `waitUntil`.\n//!\n//! # Gotchas\n//! Marks and deadlines are meaningful only on this provider's monotonic timeline. Native waits\n//! block the calling host thread; they do not park one Fiber. Failure, malformed host output, or\n//! deadline overflow is a fatal trap. Linux requires `glibc` 2.17 or later and excludes system\n//! suspend time from this clock. macOS includes system suspend time. Direct WebAssembly has no\n//! ambient implementation.\n\nimport silk.i64 as i64\nimport silk.monotonic_clock as MonotonicClock\nimport silk.option { None, Option, Some }\nimport silk.system_clock as SystemClock\nimport silk.system_clock { Instant }\nimport silk.u64 as u64\n\n/// A stateless Unix-family native provider of monotonic marks and blocking deadlines.\n///\n/// # Gotchas\n///\n/// A failed, invalid, or unrepresentable clock result traps. Direct WebAssembly does not support a\n/// reachable operation on this provider.\npub struct OsMonotonicClock {}\n\n/// Creates a native monotonic-clock provider without reading the clock or installing a default.\npub fn make() -> OsMonotonicClock {\n return OsMonotonicClock {}\n}\n\nfn invalidI64() -> i64 {\n let invalid = 1 / 0\n return invalidI64()\n}\n\nfn requireI64(value: Option) -> i64 {\n return match move value {\n None {} => invalidI64()\n Some { value: present } => present\n }\n}\n\neffect fn read(seconds: &mut i64, nanoseconds: &mut i64) -> bool {\n unsafe { return run Intrinsic.osMonotonicClockNow(seconds, nanoseconds) }\n return false\n}\n\neffect fn readResolution(nanoseconds: &mut u64) -> bool {\n unsafe { return run Intrinsic.osMonotonicClockResolution(nanoseconds) }\n return false\n}\n\neffect fn waitAt(seconds: i64, nanoseconds: i64) -> bool {\n unsafe { return run Intrinsic.osMonotonicClockWaitUntil(seconds, nanoseconds) }\n return false\n}\n\neffect fn readInstant() -> Instant {\n let mut seconds = i64.toI64(0)\n let mut nanoseconds = i64.toI64(0)\n let succeeded = run read(&mut seconds, &mut nanoseconds)\n if succeeded { return SystemClock.make(seconds, nanoseconds) }\n let invalid = 1 / 0\n return SystemClock.make(seconds, nanoseconds)\n}\n\neffect fn now(self: &mut OsMonotonicClock) -> Instant {\n return run readInstant()\n}\n\neffect fn getResolution(self: &mut OsMonotonicClock) -> u64 {\n let mut nanoseconds = u64.MIN\n let succeeded = run readResolution(&mut nanoseconds)\n if succeeded {\n if nanoseconds > u64.MIN { return nanoseconds }\n }\n let invalid = 1 / 0\n return nanoseconds\n}\n\neffect fn waitDeadline(when: Instant) -> () {\n let mut deadline = move when\n let seconds = SystemClock.seconds(&deadline)\n let nanoseconds = SystemClock.nanoseconds(&deadline)\n let succeeded = run waitAt(seconds, nanoseconds)\n if succeeded { return () }\n let invalid = 1 / 0\n return ()\n}\n\neffect fn waitUntil(self: &mut OsMonotonicClock, when: Instant) -> () {\n return run waitDeadline(move when)\n}\n\neffect fn waitFor(self: &mut OsMonotonicClock, howLong: u64) -> () {\n let mut start = run readInstant()\n let startSeconds = SystemClock.seconds(&start)\n let startNanoseconds = SystemClock.nanoseconds(&start)\n let billion = u64.toU64(1000000000)\n let wholeSeconds = requireI64(u64.checkedToI64(howLong / billion))\n let fraction = u64.toI64(howLong % billion)\n let mut deadlineNanoseconds = startNanoseconds + fraction\n let mut carry = i64.toI64(0)\n if deadlineNanoseconds >= 1000000000 {\n deadlineNanoseconds = deadlineNanoseconds - 1000000000\n carry = i64.toI64(1)\n }\n let extended = requireI64(i64.checkedAdd(startSeconds, wholeSeconds))\n let deadlineSeconds = requireI64(i64.checkedAdd(extended, carry))\n let deadline = SystemClock.make(deadlineSeconds, deadlineNanoseconds)\n return run waitDeadline(move deadline)\n}\n\nimpl MonotonicClock.MonotonicClock for OsMonotonicClock {\n now: OsMonotonicClock.now\n getResolution: OsMonotonicClock.getResolution\n waitUntil: OsMonotonicClock.waitUntil\n waitFor: OsMonotonicClock.waitFor\n}\n", + "//! Native monotonic clock provider with source-derived relative waiting.\n//!\n//! # When to use\n//! Construct [`OsMonotonicClock`] at a native application edge for elapsed-time measurement and\n//! blocking deadlines. Use a scripted provider when tests need virtual advancement.\n//!\n//! # Details\n//! Construction performs no host call. Reads, resolution queries, and absolute waits use three\n//! matching native intrinsics. `waitFor` performs exactly one starting read and uses checked\n//! split-field deadline arithmetic. It then uses the same absolute-wait operation as `waitUntil`.\n//!\n//! # Gotchas\n//! Marks and deadlines are meaningful only on this provider's monotonic timeline. Native waits\n//! block the calling host thread; they do not park one Fiber. Failure, malformed host output, or\n//! deadline overflow is a fatal trap. Linux requires `glibc` 2.17 or later and excludes system\n//! suspend time from this clock. macOS includes system suspend time. Direct WebAssembly has no\n//! ambient implementation.\n\nimport silk.i64 as i64\nimport silk.monotonic_clock as MonotonicClock\nimport silk.option { Option }\nimport silk.system_clock as SystemClock\nimport silk.system_clock { Instant }\nimport silk.u64 as u64\n\n/// A stateless Unix-family native provider of monotonic marks and blocking deadlines.\n///\n/// # Gotchas\n///\n/// A failed, invalid, or unrepresentable clock result traps. Direct WebAssembly does not support a\n/// reachable operation on this provider.\npub struct OsMonotonicClock {}\n\n/// Creates a native monotonic-clock provider without reading the clock or installing a default.\npub fn make() -> OsMonotonicClock {\n return OsMonotonicClock {}\n}\n\nfn invalidI64() -> i64 {\n let invalid = 1 / 0\n return invalidI64()\n}\n\nfn requireI64(value: Option) -> i64 {\n return match move value {\n Option.None => invalidI64()\n Option.Some { value: present } => present\n }\n}\n\neffect fn read(seconds: &mut i64, nanoseconds: &mut i64) -> bool {\n unsafe { return run Intrinsic.osMonotonicClockNow(seconds, nanoseconds) }\n return false\n}\n\neffect fn readResolution(nanoseconds: &mut u64) -> bool {\n unsafe { return run Intrinsic.osMonotonicClockResolution(nanoseconds) }\n return false\n}\n\neffect fn waitAt(seconds: i64, nanoseconds: i64) -> bool {\n unsafe { return run Intrinsic.osMonotonicClockWaitUntil(seconds, nanoseconds) }\n return false\n}\n\neffect fn readInstant() -> Instant {\n let mut seconds = i64.toI64(0)\n let mut nanoseconds = i64.toI64(0)\n let succeeded = run read(&mut seconds, &mut nanoseconds)\n if succeeded { return SystemClock.make(seconds, nanoseconds) }\n let invalid = 1 / 0\n return SystemClock.make(seconds, nanoseconds)\n}\n\neffect fn now(self: &mut OsMonotonicClock) -> Instant {\n return run readInstant()\n}\n\neffect fn getResolution(self: &mut OsMonotonicClock) -> u64 {\n let mut nanoseconds = u64.MIN\n let succeeded = run readResolution(&mut nanoseconds)\n if succeeded {\n if nanoseconds > u64.MIN { return nanoseconds }\n }\n let invalid = 1 / 0\n return nanoseconds\n}\n\neffect fn waitDeadline(when: Instant) -> () {\n let mut deadline = move when\n let seconds = SystemClock.seconds(&deadline)\n let nanoseconds = SystemClock.nanoseconds(&deadline)\n let succeeded = run waitAt(seconds, nanoseconds)\n if succeeded { return () }\n let invalid = 1 / 0\n return ()\n}\n\neffect fn waitUntil(self: &mut OsMonotonicClock, when: Instant) -> () {\n return run waitDeadline(move when)\n}\n\neffect fn waitFor(self: &mut OsMonotonicClock, howLong: u64) -> () {\n let mut start = run readInstant()\n let startSeconds = SystemClock.seconds(&start)\n let startNanoseconds = SystemClock.nanoseconds(&start)\n let billion = u64.toU64(1000000000)\n let wholeSeconds = requireI64(u64.checkedToI64(howLong / billion))\n let fraction = u64.toI64(howLong % billion)\n let mut deadlineNanoseconds = startNanoseconds + fraction\n let mut carry = i64.toI64(0)\n if deadlineNanoseconds >= 1000000000 {\n deadlineNanoseconds = deadlineNanoseconds - 1000000000\n carry = i64.toI64(1)\n }\n let extended = requireI64(i64.checkedAdd(startSeconds, wholeSeconds))\n let deadlineSeconds = requireI64(i64.checkedAdd(extended, carry))\n let deadline = SystemClock.make(deadlineSeconds, deadlineNanoseconds)\n return run waitDeadline(move deadline)\n}\n\nimpl MonotonicClock.MonotonicClock for OsMonotonicClock {\n now: OsMonotonicClock.now\n getResolution: OsMonotonicClock.getResolution\n waitUntil: OsMonotonicClock.waitUntil\n waitFor: OsMonotonicClock.waitFor\n}\n", }, { module: 'silk/os_random', @@ -917,14 +917,14 @@ export const modules = [ module: 'silk/os_standard_input', path: 'silk/os_standard_input.silk', sourceIdentity: 'silk/os_standard_input', - digest: 'fb05c45246e630fbdc3fe4dad1defa42974a07ecd26521fa850de252b56438c3', + digest: 'ff1b2df9217443ef2c8286293d4e4712991bf50ba3dfb8270481a48a7d9fef51', documentation: 'silk/os_standard_input.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], runtimeInventory: ['osStandardInputRead'], namespace: 'OsStandardInput', source: - "//! Native [`StandardInput`] provider backed by the process standard-input descriptor.\n//!\n//! # When to use\n//! Construct [`OsStandardInput`] at a native application edge and provide it to portable byte-input\n//! code. Use a scripted provider in tests to control partial reads, end-of-input, and failures.\n//!\n//! # Details\n//! The provider owns no persistent state and commits each host read directly into the caller's\n//! buffer. For a non-empty buffer, a zero-length host transfer becomes the outcome selected by\n//! [`endOfInput`]. Only a host read error becomes [`StreamReadError`]. Partial transfer counts are\n//! preserved exactly.\n//!\n//! Constructing the provider performs no read. Portable code reads after the application supplies\n//! `&mut OsStandardInput` for the `&mut StandardInput` requirement.\n//!\n//! # Gotchas\n//! Reachable OS standard-input operations are native-only. Direct WebAssembly compilation rejects\n//! them instead of inventing a descriptor import. Evaluator execution requires an injected adapter.\n//! The caller must use a non-empty buffer. A zero-capacity host read also transfers zero bytes.\n//!\n//! # Examples\n//! ## Construct the native provider without reading standard input\n//!\n//! ```silk\n//! import silk.os_standard_input as OsStandardInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsStandardInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.i32 as i32\nimport silk.option { None, Option, Some, none }\nimport silk.standard_input {\n ReadOutcome,\n StandardInput,\n StreamReadError,\n endOfInput,\n filled,\n readFailure\n}\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`StandardInput`] provider for the process input descriptor.\n///\n/// # Details\n///\n/// The process owns the descriptor. Each read changes only the committed prefix of the caller's\n/// buffer and preserves the host transfer count.\npub struct OsStandardInput {}\n\n/// Creates a stateless provider for native standard-input bytes.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut StandardInput` to\n/// portable code that calls `silk.standard_input.receive`.\n///\n/// # Details\n///\n/// Construction performs no read and cannot fail. For a non-empty read buffer, a later zero-byte\n/// host transfer becomes end-of-input data. A host read error becomes [`StreamReadError`].\npub fn make() -> OsStandardInput {\n return OsStandardInput {}\n}\n\neffect fn rawRead(output: &mut [u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osStandardInputRead(output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn raise() -> never ! StreamReadError {\n fail readFailure()\n}\n\n/// Commits one host read into the caller's buffer.\n///\n/// The low-level boundary reports a zero-length transfer for the end of input, which becomes\n/// `EndOfInput` data rather than a typed failure. Only a host error becomes `StreamReadError`.\n///\n/// The caller must use a non-empty `buffer`. With no capacity, the host also reports a zero-length\n/// transfer and cannot prove permanent end-of-input.\neffect fn read(self: &mut OsStandardInput, buffer: &mut [u8]) -> ReadOutcome ! StreamReadError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let received = run rawRead(move buffer, &mut lowReason, &mut nativeCode)\n let length = match move received {\n None {} => run raise()\n Some { value: selected } => selected\n }\n if length == usize.ZERO { return endOfInput() }\n return filled(length)\n}\n\nimpl StandardInput for OsStandardInput {\n read: OsStandardInput.read\n}\n", + "//! Native [`StandardInput`] provider backed by the process standard-input descriptor.\n//!\n//! # When to use\n//! Construct [`OsStandardInput`] at a native application edge and provide it to portable byte-input\n//! code. Use a scripted provider in tests to control partial reads, end-of-input, and failures.\n//!\n//! # Details\n//! The provider owns no persistent state and commits each host read directly into the caller's\n//! buffer. For a non-empty buffer, a zero-length host transfer becomes the outcome selected by\n//! [`endOfInput`]. Only a host read error becomes [`StreamReadError`]. Partial transfer counts are\n//! preserved exactly.\n//!\n//! Constructing the provider performs no read. Portable code reads after the application supplies\n//! `&mut OsStandardInput` for the `&mut StandardInput` requirement.\n//!\n//! # Gotchas\n//! Reachable OS standard-input operations are native-only. Direct WebAssembly compilation rejects\n//! them instead of inventing a descriptor import. Evaluator execution requires an injected adapter.\n//! The caller must use a non-empty buffer. A zero-capacity host read also transfers zero bytes.\n//!\n//! # Examples\n//! ## Construct the native provider without reading standard input\n//!\n//! ```silk\n//! import silk.os_standard_input as OsStandardInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsStandardInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.i32 as i32\nimport silk.option { Option, none }\nimport silk.standard_input {\n ReadOutcome,\n StandardInput,\n StreamReadError,\n endOfInput,\n filled,\n readFailure\n}\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`StandardInput`] provider for the process input descriptor.\n///\n/// # Details\n///\n/// The process owns the descriptor. Each read changes only the committed prefix of the caller's\n/// buffer and preserves the host transfer count.\npub struct OsStandardInput {}\n\n/// Creates a stateless provider for native standard-input bytes.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut StandardInput` to\n/// portable code that calls `silk.standard_input.receive`.\n///\n/// # Details\n///\n/// Construction performs no read and cannot fail. For a non-empty read buffer, a later zero-byte\n/// host transfer becomes end-of-input data. A host read error becomes [`StreamReadError`].\npub fn make() -> OsStandardInput {\n return OsStandardInput {}\n}\n\neffect fn rawRead(output: &mut [u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osStandardInputRead(output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn raise() -> never ! StreamReadError {\n fail readFailure()\n}\n\n/// Commits one host read into the caller's buffer.\n///\n/// The low-level boundary reports a zero-length transfer for the end of input, which becomes\n/// `EndOfInput` data rather than a typed failure. Only a host error becomes `StreamReadError`.\n///\n/// The caller must use a non-empty `buffer`. With no capacity, the host also reports a zero-length\n/// transfer and cannot prove permanent end-of-input.\neffect fn read(self: &mut OsStandardInput, buffer: &mut [u8]) -> ReadOutcome ! StreamReadError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let received = run rawRead(move buffer, &mut lowReason, &mut nativeCode)\n let length = match move received {\n Option.None => run raise()\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO { return endOfInput() }\n return filled(length)\n}\n\nimpl StandardInput for OsStandardInput {\n read: OsStandardInput.read\n}\n", }, { module: 'silk/os_system_clock', @@ -976,13 +976,13 @@ export const modules = [ module: 'silk/result', path: 'silk/result.silk', sourceIdentity: 'silk/result', - digest: '1627738c89c3e0cba324b0b92b9e36570f1962e268b27a5b80b889b40f4d510a', + digest: '59cbb573d593e4d5438d8b60812470d45fe1071c6c8bc2d80cb62ac1daf72deb', documentation: 'silk/result.silk', layer: 'portable', runtimeInventory: [], namespace: 'Result', source: - '//! Completed success-or-failure values that can be inspected and transformed as ordinary data.\n//!\n//! # When to use\n//! Use [`Result`] after an effectful computation has been reified, or whenever both outcome arms\n//! belong in a value. Use [`map`] for success, [`mapError`] for failure, and [`flatMap`] for a\n//! success continuation that already returns a result.\n//!\n//! # Details\n//! `Result` owns either [`Success`] or [`Failure`]. Its combinators move the selected payload\n//! forward and preserve the other arm without inventing a runtime failure-row descriptor.\n//!\n//! Unlike an `Effect`, a `Result` is already completed ordinary data: it does not run,\n//! require a provider, or propagate through `fail`. Use `Effect.result` to turn one Effect execution\n//! into a `Result` when a caller needs to inspect or store the outcome.\n//!\n//! # Examples\n//! ## Transform a success and choose a fallback for failure\n//! ```silk\n//! import silk.result as Result\n//!\n//! fn half(value: i32) -> Result.Result {\n//! if value == 0 {\n//! return Result.failResult(2)\n//! }\n//! return Result.succeed(value / 2)\n//! }\n//!\n//! fn addTwo(value: i32) -> i32 {\n//! return value + 2\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Result.succeed(80)\n//! let halved = Result.flatMap(move initial, half)\n//! let answer = Result.map(move halved, addTwo)\n//! let failed = Result.failResult(7)\n//! if Result.isFailure(&failed) {} else {\n//! return 0\n//! }\n//! return Result.unwrapOr(move answer, 0)\n//! }\n//! ```\n\n// Canonical completed typed outcome data. Failure rows project to ordinary value sums through\n// E directly; Result itself remains ordinary source-defined data with no runtime row descriptor.\n\nimport silk.bool as bool\n\n/// The successful member of a completed [`Result`].\npub struct Success {\n /// The produced success value.\n value: A\n}\n\n/// The failed member of a completed [`Result`].\npub struct Failure {\n /// The produced failure value.\n error: F\n}\n\n/// One completed outcome: either a success carrying `A` or a failure carrying `F`.\n///\n/// # Details\n///\n/// `Result` is the reified form of an Effect that has already run. Reifying an Effect turns its\n/// failure row into ordinary value data, which is what lets the failure combinators in\n/// `silk.effect` be written as ordinary Silk source instead of compiler built-ins.\n/// A `Result` is consumed when matched or passed to a transforming combinator; borrow it for\n/// [`isSuccess`] and [`isFailure`] when the payload must remain available.\npub struct Result {\n /// The completed outcome, narrowed with `match`.\n value: Success | Failure\n}\n\n/// Constructs a completed success by moving `value` into the success arm.\npub fn succeed(value: A) -> Result {\n return Result { value: Success { value: move value } }\n}\n\n/// Constructs a completed failure by moving `error` into the failure arm.\npub fn failResult(error: F) -> Result {\n return Result { value: Failure { error: move error } }\n}\n\n/// Applies `transform` once to a success value and carries a failure through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Failure`]. This consumes the result and may change only its\n/// success type; use [`mapError`] to change the failure type instead.\npub fn map(self: Result, transform: once fn(A) -> B) -> Result {\n return match move self {\n Result { value: outcome } => match move outcome {\n Success { value } => succeed(transform(move value))\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Applies `transform` once to a failure value and carries a success through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Success`]. This consumes the result and may change only its\n/// failure type.\npub fn mapError(self: Result, transform: once fn(F) -> G) -> Result {\n return match move self {\n Result { value: outcome } => match move outcome {\n Success { value } => succeed(move value)\n Failure { error } => failResult(transform(move error))\n }\n }\n}\n\n/// Continues a success with a transform that answers with a Result of its own, so the outcome\n/// stays one Result deep instead of nesting.\n///\n/// # Details\n///\n/// A failure bypasses the callback unchanged. The callback must use the same failure type `F`, so\n/// use [`mapError`] before or after this operation when the steps use different error types.\npub fn flatMap(self: Result, transform: once fn(A) -> Result) -> Result {\n return match move self {\n Result { value: outcome } => match move outcome {\n Success { value } => transform(move value)\n Failure { error } => failResult(move error)\n }\n }\n}\n\n/// Returns the success value, or the fallback value when the outcome is a failure.\n///\n/// # Details\n///\n/// Only the failure arm consumes the fallback. The success arm releases it, and the failure arm\n/// releases the error, so exactly one owned value leaves this call and the other drops.\n/// Use `match` instead when the failure payload affects recovery or must be retained.\npub fn unwrapOr(\n self: Result,\n /// The owned alternative consumed only when `self` is a failure.\n fallback: A,\n) -> A {\n return match move self {\n Result { value: outcome } => match move outcome {\n Success { value } => keepSuccess(move value, move fallback)\n Failure { error } => keepFallback(move fallback, move error)\n }\n }\n}\n\n/// Returns `true` when the borrowed outcome is [`Success`], without consuming either payload.\npub fn isSuccess(self: &Result) -> bool {\n return match &self.value {\n Success succeeded => true\n Failure failed => false\n }\n}\n\n/// Returns `true` when the borrowed outcome is [`Failure`], without consuming either payload.\npub fn isFailure(self: &Result) -> bool {\n return match &self.value {\n Success succeeded => false\n Failure failed => true\n }\n}\n\n/// Releases the fallback that a success value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepSuccess(success: A, unused: A) -> A {\n drop unused\n return move success\n}\n\n/// Releases the error the failure arm carried, and answers with the fallback it did need.\nfn keepFallback(fallback: A, error: F) -> A {\n drop error\n return move fallback\n}\n', + '//! Completed success-or-failure values that can be inspected and transformed as ordinary data.\n//!\n//! # When to use\n//! Use [`Result`] after an effectful computation has been reified, or whenever both outcome arms\n//! belong in a value. Use [`map`] for success, [`mapError`] for failure, and [`flatMap`] for a\n//! success continuation that already returns a result.\n//!\n//! # Details\n//! `Result` owns either [`Success`] or [`Failure`]. Its combinators move the selected payload\n//! forward and preserve the other arm without inventing a runtime failure-row descriptor.\n//!\n//! Unlike an `Effect`, a `Result` is already completed ordinary data: it does not run,\n//! require a provider, or propagate through `fail`. Use `Effect.result` to turn one Effect execution\n//! into a `Result` when a caller needs to inspect or store the outcome.\n//!\n//! # Examples\n//! ## Transform a success and choose a fallback for failure\n//! ```silk\n//! import silk.result as Result\n//!\n//! fn half(value: i32) -> Result.Result {\n//! if value == 0 {\n//! return Result.failResult(2)\n//! }\n//! return Result.succeed(value / 2)\n//! }\n//!\n//! fn addTwo(value: i32) -> i32 {\n//! return value + 2\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Result.succeed(80)\n//! let halved = Result.flatMap(move initial, half)\n//! let answer = Result.map(move halved, addTwo)\n//! return Result.unwrapOr(move answer, 0)\n//! }\n//! ```\n\n// Canonical completed typed outcome data. Failure rows project to ordinary value sums through\n// E directly; Result itself remains ordinary source-defined data with no runtime row descriptor.\n\nimport silk.bool as bool\n\n/// One completed outcome: either a success carrying `A` or a failure carrying `F`.\n///\n/// # Details\n///\n/// `Result` is the reified form of an Effect that has already run. Reifying an Effect turns its\n/// failure row into ordinary value data, which is what lets the failure combinators in\n/// `silk.effect` be written as ordinary Silk source instead of compiler built-ins.\n/// A `Result` is consumed when matched or passed to a transforming combinator. Use a borrowed\n/// match when the payload must remain available.\npub union Result {\n /// A completed success.\n Success {\n /// The produced success value.\n value: A\n },\n /// A completed failure.\n Failure {\n /// The produced failure value.\n error: F\n }\n}\n\n/// Constructs a completed success by moving `value` into the success arm.\npub fn succeed(value: A) -> Result {\n return Result.Success { value: move value }\n}\n\n/// Constructs a completed failure by moving `error` into the failure arm.\npub fn failResult(error: F) -> Result {\n return Result.Failure { error: move error }\n}\n\n/// Applies `transform` once to a success value and carries a failure through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Failure`]. This consumes the result and may change only its\n/// success type; use [`mapError`] to change the failure type instead.\npub fn map(self: Result, transform: once fn(A) -> B) -> Result {\n return match move self {\n Result.Success { value } => succeed(transform(move value))\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Applies `transform` once to a failure value and carries a success through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Success`]. This consumes the result and may change only its\n/// failure type.\npub fn mapError(self: Result, transform: once fn(F) -> G) -> Result {\n return match move self {\n Result.Success { value } => succeed(move value)\n Result.Failure { error } => failResult(transform(move error))\n }\n}\n\n/// Continues a success with a transform that answers with a Result of its own, so the outcome\n/// stays one Result deep instead of nesting.\n///\n/// # Details\n///\n/// A failure bypasses the callback unchanged. The callback must use the same failure type `F`, so\n/// use [`mapError`] before or after this operation when the steps use different error types.\npub fn flatMap(self: Result, transform: once fn(A) -> Result) -> Result {\n return match move self {\n Result.Success { value } => transform(move value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Returns the success value, or the fallback value when the outcome is a failure.\n///\n/// # Details\n///\n/// Only the failure arm consumes the fallback. The success arm releases it, and the failure arm\n/// releases the error, so exactly one owned value leaves this call and the other drops.\n/// Use `match` instead when the failure payload affects recovery or must be retained.\npub fn unwrapOr(\n self: Result,\n /// The owned alternative consumed only when `self` is a failure.\n fallback: A,\n) -> A {\n return match move self {\n Result.Success { value } => keepSuccess(move value, move fallback)\n Result.Failure { error } => keepFallback(move fallback, move error)\n }\n}\n\n/// Releases the fallback that a success value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepSuccess(success: A, unused: A) -> A {\n drop unused\n return move success\n}\n\n/// Releases the error the failure arm carried, and answers with the fallback it did need.\nfn keepFallback(fallback: A, error: F) -> A {\n drop error\n return move fallback\n}\n', }, { module: 'silk/scheduler', @@ -1057,14 +1057,14 @@ export const modules = [ module: 'silk/string', path: 'silk/string.silk', sourceIdentity: 'silk/string', - digest: '6d392f56ebb55ca278315346e3f939a6d7779468d45422ee9f126d2e91df4dc3', + digest: 'cdb2de4dd1b6053b0c5b3600bea695b1c84d61e3ccedac28d3d26de67b867dc6', documentation: 'silk/string.silk', layer: 'portable', runtimeInventory: ['stringByteLength', 'stringFromUtf8Unchecked', 'stringUtf8Bytes'], namespace: 'String', aliases: ['InvalidUtf8', 'ScalarCursor', 'ScalarStep'], source: - '//! Valid UTF-8 text, including owned storage, byte validation, and scalar-by-scalar traversal.\n//!\n//! # When to use\n//! Use the built-in `string` type for borrowed text and [`String`] when text must own its storage.\n//! Use [`Bytes`] when arbitrary octets must survive without UTF-8 validation.\n//!\n//! # Details\n//! [`fromUtf8`] validates and borrows existing bytes without allocating; [`copyUtf8`] validates and\n//! owns a copy. [`append`] and [`appendOwned`] leave the original value unchanged if growth cannot\n//! allocate. Scalar cursors expose Unicode scalar values and byte offsets, not grapheme clusters.\n//!\n//! # Gotchas\n//! A [`ScalarCursor`] is meaningful only for the same unchanged string from which its traversal\n//! began. Start with [`scalarCursor`] and advance only with [`nextCursor`].\n//!\n//! # Examples\n//! ## Validate borrowed UTF-8 bytes\n//! ```silk\n//! import silk.result as Result\n//!\n//! import silk.string as String\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let valid = String.fromUtf8(b"Silk")\n//! |> Result.unwrapOr("")\n//! let invalid = String.fromUtf8(b"a\\x80")\n//! if !Result.isFailure(&invalid) {\n//! return 0\n//! }\n//! let length = String.byteLength(valid)\n//! |> usize.toI32\n//! return length + 38\n//! }\n//! ```\n//!\n//! ## Build owned text and read its first scalar\n//! ```silk\n//! import silk.char as char\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option as Option\n//!\n//! import silk.string as String\n//!\n//! import silk.u32 as u32\n//!\n//! fn scalarCode(step: String.ScalarStep) -> i32 {\n//! return String.scalarValue(&step)\n//! |> char.toU32\n//! |> u32.toI32\n//! }\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let copying = String.copy("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let mut text = run copying\n//! let appending = String.append(&mut text, "!")\n//! |> Effect.provideMut(&mut allocator)\n//! let appended = run appending\n//! let stepped = String.nextScalar(String.view(&text), String.scalarCursor())\n//! let mapped = Option.map(move stepped, scalarCode)\n//! let scalar = Option.unwrapOr(move mapped, 0)\n//! return scalar - 191\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n copy as bytesCopy,\n append as bytesAppend,\n asSlice as bytesAsSlice,\n length as bytesLength\n}\nimport silk.char as char\nimport silk.char { fromU32 as charFromU32 }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { None }\nimport silk.option { Option, none, some }\nimport silk.option { Some }\nimport silk.result { Result, failResult, succeed }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// An owned sequence of valid UTF-8 bytes that releases its storage on drop.\npub struct String {\n bytes: Bytes\n}\n\n/// The first byte offset at which UTF-8 validation failed.\npub struct InvalidUtf8 {\n /// The zero-based offset of the first byte that cannot continue a valid UTF-8 sequence.\n pub offset: usize\n}\n\n/// An opaque UTF-8 position used for scalar-by-scalar traversal.\npub struct ScalarCursor {\n byteOffset: usize\n}\n\n/// One decoded Unicode scalar, its byte offset, and the cursor after it.\npub struct ScalarStep {\n scalar: char\n byteOffset: usize\n next: ScalarCursor\n}\n\nfn byte(value: u8) -> u8 {\n return value\n}\n\nfn continuation(value: u8) -> bool {\n if value < byte(128) { return false }\n return value <= byte(191)\n}\n\n/// Returns the first byte offset at which UTF-8 validation fails, or None for complete valid text.\nfn firstInvalidUtf8(values: &[u8]) -> Option {\n let mut index = usize.ZERO\n while index < values.length {\n let first = values[index]\n if first < byte(128) {\n index = index + usize.ONE\n } else {\n if first < byte(194) { return some(index) }\n if first <= byte(223) {\n if values.length <= index + usize.ONE { return some(index) }\n if continuation(values[index + usize.ONE]) == false {\n return some(index + usize.ONE)\n }\n index = index + 2\n } else {\n if first <= byte(239) {\n if values.length <= index + 2 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if first == byte(224) {\n if second < byte(160) { return some(index + usize.ONE) }\n }\n if first == byte(237) {\n if byte(159) < second { return some(index + usize.ONE) }\n }\n index = index + 3\n } else {\n if byte(244) < first { return some(index) }\n if values.length <= index + 3 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n let fourth = values[index + 3]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if continuation(fourth) == false { return some(index + 3) }\n if first == byte(240) {\n if second < byte(144) { return some(index + usize.ONE) }\n }\n if first == byte(244) {\n if byte(143) < second { return some(index + usize.ONE) }\n }\n index = index + 4\n }\n }\n }\n }\n return none()\n}\n\n/// Borrows caller-validated UTF-8 bytes as text without runtime validation.\n///\n/// # When to use\n///\n/// Use this function only when an earlier operation proves that the complete byte view is UTF-8.\n/// Use [`fromUtf8`] when the bytes have not been validated.\n///\n/// # Gotchas\n///\n/// The caller must guarantee that the complete byte view is valid UTF-8 for the lifetime of the\n/// returned string view. Invalid bytes violate the safety contract.\npub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(values) }\n return ""\n}\n\n/// Validates a complete byte view and borrows it as text without allocating.\n///\n/// # Details\n///\n/// Success returns a `string` view with the same lexical lifetime as `values`. Failure returns the\n/// first invalid byte offset in [`InvalidUtf8`].\npub fn fromUtf8(values: &[u8]) -> Result {\n let failure = match move firstInvalidUtf8(values) {\n Some { value } => move value\n None {} => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let text = unsafe fromUtf8Unchecked(values)\n return succeed(text)\n return failResult(InvalidUtf8 { offset: usize.ZERO })\n}\n\n/// Constructs an empty owned String without allocating.\npub fn make() -> String {\n return String { bytes: bytesMake() }\n}\n\n/// Copies valid borrowed text into independently owned storage.\npub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = Intrinsic.stringUtf8Bytes(value)\n let bytes = run bytesCopy(source)\n return String { bytes: move bytes }\n}\n\n/// Validates complete UTF-8 bytes and copies them into independently owned storage.\n///\n/// # When to use\n///\n/// Use this function when the bytes must outlive their current buffer. Use [`fromUtf8`] for a\n/// borrowed result without allocation.\n///\n/// # Details\n///\n/// Invalid input returns [`InvalidUtf8`] as ordinary result data. Allocation failure remains in the\n/// Effect failure channel. No owned string is returned in either failure case.\npub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator {\n let failure = match move firstInvalidUtf8(values) {\n Some { value } => move value\n None {} => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let bytes = run bytesCopy(values)\n return succeed(String { bytes: move bytes })\n}\n\n// Appending grows the existing storage rather than copying the whole string into fresh storage\n// first. The atomicity is the same either way — the underlying byte append builds its replacement\n// buffer in full before committing, so a failed allocation leaves the original untouched — but the\n// cost is not: composing a message from several pieces is what this API is for, and a copy per\n// piece made that quadratic in the message and linear in allocations.\n/// Appends complete valid text atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function for borrowed text. Use [`appendOwned`] when the suffix is an owned [`String`].\n///\n/// # Details\n///\n/// If growth fails, `self` keeps its prior contents and byte length.\npub effect fn append(self: &mut String, value: string) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = Intrinsic.stringUtf8Bytes(value)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Appends another owned String atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function to consume an owned suffix. Use [`append`] when the suffix is borrowed text.\n///\n/// # Details\n///\n/// This function consumes `value`. If growth fails, `self` keeps its prior contents and byte length.\npub effect fn appendOwned(self: &mut String, value: String) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = bytesAsSlice(&value.bytes)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Borrows the complete owned contents as valid text without allocating or copying.\npub fn view(self: &String) -> string {\n let bytes = bytesAsSlice(&self.bytes)\n return unsafe fromUtf8Unchecked(bytes)\n}\n\n/// Borrows a string\'s immutable UTF-8 encoding.\npub fn utf8Bytes(value: string) -> &[u8] {\n return Intrinsic.stringUtf8Bytes(value)\n}\n\n/// Returns a string\'s UTF-8 byte length.\npub fn byteLength(value: string) -> usize {\n return Intrinsic.stringByteLength(value)\n}\n\n/// Borrows an owned String\'s immutable UTF-8 encoding.\npub fn ownedUtf8Bytes(self: &String) -> &[u8] {\n return bytesAsSlice(&self.bytes)\n}\n\n/// Returns an owned String\'s initialized UTF-8 byte length.\npub fn ownedByteLength(self: &String) -> usize {\n return bytesLength(&self.bytes)\n}\n\n/// Creates a cursor at UTF-8 byte offset zero, before the first Unicode scalar.\npub fn scalarCursor() -> ScalarCursor {\n return ScalarCursor { byteOffset: usize.ZERO }\n}\n\n/// Returns a cursor\'s explicit UTF-8 byte offset.\npub fn cursorByteOffset(cursor: &ScalarCursor) -> usize {\n return cursor.byteOffset\n}\n\n/// Returns the decoded Unicode scalar value without consuming the step.\npub fn scalarValue(step: &ScalarStep) -> char {\n return step.scalar\n}\n\n/// Returns the UTF-8 byte offset at which one step begins.\npub fn scalarByteOffset(step: &ScalarStep) -> usize {\n return step.byteOffset\n}\n\n/// Consumes one scalar step and returns the cursor immediately after that scalar.\npub fn nextCursor(step: ScalarStep) -> ScalarCursor {\n return match move step {\n ScalarStep { scalar, byteOffset, next } => move next\n }\n}\n\nfn scalar32(value: u8) -> u32 {\n return u8.toU32(value)\n}\n\n/// Decodes the scalar at a cursor, or returns `None` at the end of the string.\n///\n/// # Details\n///\n/// A present step contains the scalar, its starting byte offset, and the next cursor. This function\n/// does not allocate.\n///\n/// # Gotchas\n///\n/// The cursor must come from [`scalarCursor`] or [`nextCursor`] for the same unchanged string.\npub fn nextScalar(value: string, cursor: ScalarCursor) -> Option {\n let bytes = Intrinsic.stringUtf8Bytes(value)\n let offset = cursor.byteOffset\n if offset == bytes.length { return none() }\n let first = bytes[offset]\n let mut scalar = scalar32(first)\n let mut width = usize.ONE\n if byte(194) <= first {\n if first <= byte(223) {\n scalar = (scalar32(first) - u32.toU32(192)) * u32.toU32(64)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128))\n width = 2\n } else {\n if first <= byte(239) {\n scalar = (scalar32(first) - u32.toU32(224)) * u32.toU32(4096)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128))\n width = 3\n } else {\n scalar = (scalar32(first) - u32.toU32(240)) * u32.toU32(262144)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(4096)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 3]) - u32.toU32(128))\n width = 4\n }\n }\n }\n return match move charFromU32(scalar) {\n Some { value: decoded } => some(ScalarStep {\n scalar: decoded,\n byteOffset: offset,\n next: ScalarCursor { byteOffset: offset + width }\n })\n None {} => none()\n }\n}\n', + '//! Valid UTF-8 text, including owned storage, byte validation, and scalar-by-scalar traversal.\n//!\n//! # When to use\n//! Use the built-in `string` type for borrowed text and [`String`] when text must own its storage.\n//! Use [`Bytes`] when arbitrary octets must survive without UTF-8 validation.\n//!\n//! # Details\n//! [`fromUtf8`] validates and borrows existing bytes without allocating; [`copyUtf8`] validates and\n//! owns a copy. [`append`] and [`appendOwned`] leave the original value unchanged if growth cannot\n//! allocate. Scalar cursors expose Unicode scalar values and byte offsets, not grapheme clusters.\n//!\n//! # Gotchas\n//! A [`ScalarCursor`] is meaningful only for the same unchanged string from which its traversal\n//! began. Start with [`scalarCursor`] and advance only with [`nextCursor`].\n//!\n//! # Examples\n//! ## Validate borrowed UTF-8 bytes\n//! ```silk\n//! import silk.result as Result\n//!\n//! import silk.string as String\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let valid = String.fromUtf8(b"Silk")\n//! |> Result.unwrapOr("")\n//! let invalid = String.fromUtf8(b"a\\x80")\n//! match move invalid {\n//! Result.Result.Success { .. } => return 0\n//! Result.Result.Failure { .. } => ()\n//! }\n//! let length = String.byteLength(valid)\n//! |> usize.toI32\n//! return length + 38\n//! }\n//! ```\n//!\n//! ## Build owned text and read its first scalar\n//! ```silk\n//! import silk.char as char\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option as Option\n//!\n//! import silk.string as String\n//!\n//! import silk.u32 as u32\n//!\n//! fn scalarCode(step: String.ScalarStep) -> i32 {\n//! return String.scalarValue(&step)\n//! |> char.toU32\n//! |> u32.toI32\n//! }\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let copying = String.copy("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let mut text = run copying\n//! let appending = String.append(&mut text, "!")\n//! |> Effect.provideMut(&mut allocator)\n//! let appended = run appending\n//! let stepped = String.nextScalar(String.view(&text), String.scalarCursor())\n//! let mapped = Option.map(move stepped, scalarCode)\n//! let scalar = Option.unwrapOr(move mapped, 0)\n//! return scalar - 191\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n copy as bytesCopy,\n append as bytesAppend,\n asSlice as bytesAsSlice,\n length as bytesLength\n}\nimport silk.char as char\nimport silk.char { fromU32 as charFromU32 }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { Option, none, some }\nimport silk.result { Result, failResult, succeed }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// An owned sequence of valid UTF-8 bytes that releases its storage on drop.\npub struct String {\n bytes: Bytes\n}\n\n/// The first byte offset at which UTF-8 validation failed.\npub struct InvalidUtf8 {\n /// The zero-based offset of the first byte that cannot continue a valid UTF-8 sequence.\n pub offset: usize\n}\n\n/// An opaque UTF-8 position used for scalar-by-scalar traversal.\npub struct ScalarCursor {\n byteOffset: usize\n}\n\n/// One decoded Unicode scalar, its byte offset, and the cursor after it.\npub struct ScalarStep {\n scalar: char\n byteOffset: usize\n next: ScalarCursor\n}\n\nfn byte(value: u8) -> u8 {\n return value\n}\n\nfn continuation(value: u8) -> bool {\n if value < byte(128) { return false }\n return value <= byte(191)\n}\n\n/// Returns the first byte offset at which UTF-8 validation fails, or None for complete valid text.\nfn firstInvalidUtf8(values: &[u8]) -> Option {\n let mut index = usize.ZERO\n while index < values.length {\n let first = values[index]\n if first < byte(128) {\n index = index + usize.ONE\n } else {\n if first < byte(194) { return some(index) }\n if first <= byte(223) {\n if values.length <= index + usize.ONE { return some(index) }\n if continuation(values[index + usize.ONE]) == false {\n return some(index + usize.ONE)\n }\n index = index + 2\n } else {\n if first <= byte(239) {\n if values.length <= index + 2 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if first == byte(224) {\n if second < byte(160) { return some(index + usize.ONE) }\n }\n if first == byte(237) {\n if byte(159) < second { return some(index + usize.ONE) }\n }\n index = index + 3\n } else {\n if byte(244) < first { return some(index) }\n if values.length <= index + 3 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n let fourth = values[index + 3]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if continuation(fourth) == false { return some(index + 3) }\n if first == byte(240) {\n if second < byte(144) { return some(index + usize.ONE) }\n }\n if first == byte(244) {\n if byte(143) < second { return some(index + usize.ONE) }\n }\n index = index + 4\n }\n }\n }\n }\n return none()\n}\n\n/// Borrows caller-validated UTF-8 bytes as text without runtime validation.\n///\n/// # When to use\n///\n/// Use this function only when an earlier operation proves that the complete byte view is UTF-8.\n/// Use [`fromUtf8`] when the bytes have not been validated.\n///\n/// # Gotchas\n///\n/// The caller must guarantee that the complete byte view is valid UTF-8 for the lifetime of the\n/// returned string view. Invalid bytes violate the safety contract.\npub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(values) }\n return ""\n}\n\n/// Validates a complete byte view and borrows it as text without allocating.\n///\n/// # Details\n///\n/// Success returns a `string` view with the same lexical lifetime as `values`. Failure returns the\n/// first invalid byte offset in [`InvalidUtf8`].\npub fn fromUtf8(values: &[u8]) -> Result {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let text = unsafe fromUtf8Unchecked(values)\n return succeed(text)\n return failResult(InvalidUtf8 { offset: usize.ZERO })\n}\n\n/// Constructs an empty owned String without allocating.\npub fn make() -> String {\n return String { bytes: bytesMake() }\n}\n\n/// Copies valid borrowed text into independently owned storage.\npub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = Intrinsic.stringUtf8Bytes(value)\n let bytes = run bytesCopy(source)\n return String { bytes: move bytes }\n}\n\n/// Validates complete UTF-8 bytes and copies them into independently owned storage.\n///\n/// # When to use\n///\n/// Use this function when the bytes must outlive their current buffer. Use [`fromUtf8`] for a\n/// borrowed result without allocation.\n///\n/// # Details\n///\n/// Invalid input returns [`InvalidUtf8`] as ordinary result data. Allocation failure remains in the\n/// Effect failure channel. No owned string is returned in either failure case.\npub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let bytes = run bytesCopy(values)\n return succeed(String { bytes: move bytes })\n}\n\n// Appending grows the existing storage rather than copying the whole string into fresh storage\n// first. The atomicity is the same either way — the underlying byte append builds its replacement\n// buffer in full before committing, so a failed allocation leaves the original untouched — but the\n// cost is not: composing a message from several pieces is what this API is for, and a copy per\n// piece made that quadratic in the message and linear in allocations.\n/// Appends complete valid text atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function for borrowed text. Use [`appendOwned`] when the suffix is an owned [`String`].\n///\n/// # Details\n///\n/// If growth fails, `self` keeps its prior contents and byte length.\npub effect fn append(self: &mut String, value: string) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = Intrinsic.stringUtf8Bytes(value)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Appends another owned String atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function to consume an owned suffix. Use [`append`] when the suffix is borrowed text.\n///\n/// # Details\n///\n/// This function consumes `value`. If growth fails, `self` keeps its prior contents and byte length.\npub effect fn appendOwned(self: &mut String, value: String) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = bytesAsSlice(&value.bytes)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Borrows the complete owned contents as valid text without allocating or copying.\npub fn view(self: &String) -> string {\n let bytes = bytesAsSlice(&self.bytes)\n return unsafe fromUtf8Unchecked(bytes)\n}\n\n/// Borrows a string\'s immutable UTF-8 encoding.\npub fn utf8Bytes(value: string) -> &[u8] {\n return Intrinsic.stringUtf8Bytes(value)\n}\n\n/// Returns a string\'s UTF-8 byte length.\npub fn byteLength(value: string) -> usize {\n return Intrinsic.stringByteLength(value)\n}\n\n/// Borrows an owned String\'s immutable UTF-8 encoding.\npub fn ownedUtf8Bytes(self: &String) -> &[u8] {\n return bytesAsSlice(&self.bytes)\n}\n\n/// Returns an owned String\'s initialized UTF-8 byte length.\npub fn ownedByteLength(self: &String) -> usize {\n return bytesLength(&self.bytes)\n}\n\n/// Creates a cursor at UTF-8 byte offset zero, before the first Unicode scalar.\npub fn scalarCursor() -> ScalarCursor {\n return ScalarCursor { byteOffset: usize.ZERO }\n}\n\n/// Returns a cursor\'s explicit UTF-8 byte offset.\npub fn cursorByteOffset(cursor: &ScalarCursor) -> usize {\n return cursor.byteOffset\n}\n\n/// Returns the decoded Unicode scalar value without consuming the step.\npub fn scalarValue(step: &ScalarStep) -> char {\n return step.scalar\n}\n\n/// Returns the UTF-8 byte offset at which one step begins.\npub fn scalarByteOffset(step: &ScalarStep) -> usize {\n return step.byteOffset\n}\n\n/// Consumes one scalar step and returns the cursor immediately after that scalar.\npub fn nextCursor(step: ScalarStep) -> ScalarCursor {\n return match move step {\n ScalarStep { scalar, byteOffset, next } => move next\n }\n}\n\nfn scalar32(value: u8) -> u32 {\n return u8.toU32(value)\n}\n\n/// Decodes the scalar at a cursor, or returns `None` at the end of the string.\n///\n/// # Details\n///\n/// A present step contains the scalar, its starting byte offset, and the next cursor. This function\n/// does not allocate.\n///\n/// # Gotchas\n///\n/// The cursor must come from [`scalarCursor`] or [`nextCursor`] for the same unchanged string.\npub fn nextScalar(value: string, cursor: ScalarCursor) -> Option {\n let bytes = Intrinsic.stringUtf8Bytes(value)\n let offset = cursor.byteOffset\n if offset == bytes.length { return none() }\n let first = bytes[offset]\n let mut scalar = scalar32(first)\n let mut width = usize.ONE\n if byte(194) <= first {\n if first <= byte(223) {\n scalar = (scalar32(first) - u32.toU32(192)) * u32.toU32(64)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128))\n width = 2\n } else {\n if first <= byte(239) {\n scalar = (scalar32(first) - u32.toU32(224)) * u32.toU32(4096)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128))\n width = 3\n } else {\n scalar = (scalar32(first) - u32.toU32(240)) * u32.toU32(262144)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(4096)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 3]) - u32.toU32(128))\n width = 4\n }\n }\n }\n return match move charFromU32(scalar) {\n Option.Some { value: decoded } => some(ScalarStep {\n scalar: decoded,\n byteOffset: offset,\n next: ScalarCursor { byteOffset: offset + width }\n })\n Option.None => none()\n }\n}\n', }, { module: 'silk/system_clock', @@ -1083,7 +1083,7 @@ export const modules = [ module: 'silk/u16', path: 'silk/u16.silk', sourceIdentity: 'silk/u16', - digest: 'cc7d30825051674dd21ed00b4263a8124b65157a6f2b3cf77af79b29f964afcb', + digest: 'e5e50671387e27a6005e2ec95f594574a6ddcb6d67d955de663a007687cd82de', documentation: 'silk/u16.silk', layer: 'portable', runtimeInventory: [ @@ -1142,13 +1142,13 @@ export const modules = [ ], namespace: 'u16', source: - '//! Sixteen-bit unsigned integers with explicit arithmetic and conversion failure policies.\n//!\n//! # When to use\n//! Use `u16` when a file format, protocol, or compact data structure specifies exactly sixteen\n//! unsigned bits. Use `usize` for in-memory lengths and indices that follow the target.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` reports invalid arithmetic with [`Option`], `wrapping*` computes modulo 2^16, and\n//! `saturating*` clamps at [`MIN`] or [`MAX`]. Right shift inserts zero bits.\n//!\n//! Decimal [`parse`] consumes the entire input and distinguishes malformed text from range\n//! overflow. [`toText`] allocates a new owned string.\n//!\n//! # Examples\n//! ## Recover from a narrowing conversion\n//! ```silk\n//! import silk.option as Option\n//!\n//! import silk.u16 as u16\n//!\n//! import silk.u8 as u8\n//!\n//! pub fn main() -> i32 {\n//! let narrowed = u16.checkedToU8(300)\n//! let value = move narrowed\n//! |> Option.unwrapOr(42)\n//! return u8.toI32(value)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `u16` value.\npub const MAX: u16 = 65535\n\n/// The smallest `u16` value.\npub const MIN: u16 = 0\n\n/// The fixed width of `u16`, in bits.\npub const BITS: u32 = 16\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: u16) -> u8 {\n return Intrinsic.u16ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: u16) -> Option {\n return Intrinsic.u16CheckedToU8(value)\n}\n\n/// Returns `value` unchanged as `u16`. Use this function when generic conversion code\n/// can select `u16` as both source and destination.\npub fn toU16(value: u16) -> u16 {\n return Intrinsic.u16ToU16(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u16`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU16(value: u16) -> Option {\n return Intrinsic.u16CheckedToU16(value)\n}\n\n/// Converts `value` exactly to `u32`. Every `u16` value is representable.\npub fn toU32(value: u16) -> u32 {\n return Intrinsic.u16ToU32(value)\n}\n\n/// Converts `value` exactly to `u32` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToU32(value: u16) -> Option {\n return Intrinsic.u16CheckedToU32(value)\n}\n\n/// Converts `value` exactly to `u64`. Every `u16` value is representable.\npub fn toU64(value: u16) -> u64 {\n return Intrinsic.u16ToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToU64(value: u16) -> Option {\n return Intrinsic.u16CheckedToU64(value)\n}\n\n/// Converts `value` exactly to `usize`. Every `u16` value is representable.\npub fn toUsize(value: u16) -> usize {\n return Intrinsic.u16ToUsize(value)\n}\n\n/// Converts `value` exactly to `usize` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToUsize(value: u16) -> Option {\n return Intrinsic.u16CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u16) -> i8 {\n return Intrinsic.u16ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u16) -> Option {\n return Intrinsic.u16CheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: u16) -> i16 {\n return Intrinsic.u16ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: u16) -> Option {\n return Intrinsic.u16CheckedToI16(value)\n}\n\n/// Converts `value` exactly to `i32`. Every `u16` value is representable.\npub fn toI32(value: u16) -> i32 {\n return Intrinsic.u16ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToI32(value: u16) -> Option {\n return Intrinsic.u16CheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `u16` value is representable.\npub fn toI64(value: u16) -> i64 {\n return Intrinsic.u16ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToI64(value: u16) -> Option {\n return Intrinsic.u16CheckedToI64(value)\n}\n\n/// Converts `value` exactly to `isize`. Every `u16` value is representable.\npub fn toIsize(value: u16) -> isize {\n return Intrinsic.u16ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToIsize(value: u16) -> Option {\n return Intrinsic.u16CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u16) -> f32 {\n return Intrinsic.u16ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u16) -> f64 {\n return Intrinsic.u16ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u16` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u16` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u16` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u16, right: u16) -> u16 {\n return Intrinsic.u16BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u16, right: u16) -> u16 {\n return Intrinsic.u16BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u16, right: u16) -> u16 {\n return Intrinsic.u16BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u16) -> u16 {\n return Intrinsic.u16BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u16, right: u16) -> u16 {\n return Intrinsic.u16ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u16, right: u16) -> u16 {\n return Intrinsic.u16ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u16, right: u16) -> u16 {\n return Intrinsic.u16RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u16, right: u16) -> u16 {\n return Intrinsic.u16RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u16, right: u16) -> u16 {\n return Intrinsic.u16WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u16, right: u16) -> u16 {\n return Intrinsic.u16WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u16, right: u16) -> u16 {\n return Intrinsic.u16WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u16, right: u16) -> u16 {\n return Intrinsic.u16SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u16, right: u16) -> u16 {\n return Intrinsic.u16SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u16, right: u16) -> u16 {\n return Intrinsic.u16SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u16` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u16` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u16` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u16, right: u16) -> bool {\n return Intrinsic.u16Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u16, right: u16) -> bool {\n return Intrinsic.u16NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u16, right: u16) -> bool {\n return Intrinsic.u16LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u16, right: u16) -> bool {\n return Intrinsic.u16LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u16, right: u16) -> bool {\n return Intrinsic.u16GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u16, right: u16) -> bool {\n return Intrinsic.u16GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u16) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u16`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u16` range.\npub fn parse(text: string) -> Result {\n return Format.u16Value(text)\n}\n', + '//! Sixteen-bit unsigned integers with explicit arithmetic and conversion failure policies.\n//!\n//! # When to use\n//! Use `u16` when a file format, protocol, or compact data structure specifies exactly sixteen\n//! unsigned bits. Use `usize` for in-memory lengths and indices that follow the target.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` reports invalid arithmetic with [`Option`], `wrapping*` computes modulo 2^16, and\n//! `saturating*` clamps at [`MIN`] or [`MAX`]. Right shift inserts zero bits.\n//!\n//! Decimal [`parse`] consumes the entire input and distinguishes malformed text from range\n//! overflow. [`toText`] allocates a new owned string.\n//!\n//! # Examples\n//! ## Recover from a narrowing conversion\n//! ```silk\n//! import silk.option as Option\n//!\n//! import silk.u16 as u16\n//!\n//! import silk.u8 as u8\n//!\n//! pub fn main() -> i32 {\n//! let narrowed = u16.checkedToU8(300)\n//! let value = move narrowed\n//! |> Option.unwrapOr(42)\n//! return u8.toI32(value)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `u16` value.\npub const MAX: u16 = 65535\n\n/// The smallest `u16` value.\npub const MIN: u16 = 0\n\n/// The fixed width of `u16`, in bits.\npub const BITS: u32 = 16\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: u16) -> u8 {\n return Intrinsic.u16ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: u16) -> Option {\n return Intrinsic.u16CheckedToU8>(value, some, none)\n}\n\n/// Returns `value` unchanged as `u16`. Use this function when generic conversion code\n/// can select `u16` as both source and destination.\npub fn toU16(value: u16) -> u16 {\n return Intrinsic.u16ToU16(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u16`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU16(value: u16) -> Option {\n return Intrinsic.u16CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` exactly to `u32`. Every `u16` value is representable.\npub fn toU32(value: u16) -> u32 {\n return Intrinsic.u16ToU32(value)\n}\n\n/// Converts `value` exactly to `u32` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToU32(value: u16) -> Option {\n return Intrinsic.u16CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` exactly to `u64`. Every `u16` value is representable.\npub fn toU64(value: u16) -> u64 {\n return Intrinsic.u16ToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToU64(value: u16) -> Option {\n return Intrinsic.u16CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` exactly to `usize`. Every `u16` value is representable.\npub fn toUsize(value: u16) -> usize {\n return Intrinsic.u16ToUsize(value)\n}\n\n/// Converts `value` exactly to `usize` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToUsize(value: u16) -> Option {\n return Intrinsic.u16CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u16) -> i8 {\n return Intrinsic.u16ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u16) -> Option {\n return Intrinsic.u16CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: u16) -> i16 {\n return Intrinsic.u16ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: u16) -> Option {\n return Intrinsic.u16CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` exactly to `i32`. Every `u16` value is representable.\npub fn toI32(value: u16) -> i32 {\n return Intrinsic.u16ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToI32(value: u16) -> Option {\n return Intrinsic.u16CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `u16` value is representable.\npub fn toI64(value: u16) -> i64 {\n return Intrinsic.u16ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToI64(value: u16) -> Option {\n return Intrinsic.u16CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` exactly to `isize`. Every `u16` value is representable.\npub fn toIsize(value: u16) -> isize {\n return Intrinsic.u16ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `u16` value is\n/// representable.\npub fn checkedToIsize(value: u16) -> Option {\n return Intrinsic.u16CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u16) -> f32 {\n return Intrinsic.u16ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u16) -> f64 {\n return Intrinsic.u16ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u16` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u16` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u16` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u16, right: u16) -> u16 {\n return Intrinsic.u16Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u16, right: u16) -> u16 {\n return Intrinsic.u16BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u16, right: u16) -> u16 {\n return Intrinsic.u16BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u16, right: u16) -> u16 {\n return Intrinsic.u16BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u16) -> u16 {\n return Intrinsic.u16BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u16, right: u16) -> u16 {\n return Intrinsic.u16ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u16, right: u16) -> u16 {\n return Intrinsic.u16ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u16, right: u16) -> u16 {\n return Intrinsic.u16RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u16, right: u16) -> u16 {\n return Intrinsic.u16RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u16, right: u16) -> u16 {\n return Intrinsic.u16WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u16, right: u16) -> u16 {\n return Intrinsic.u16WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u16` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u16, right: u16) -> u16 {\n return Intrinsic.u16WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u16, right: u16) -> u16 {\n return Intrinsic.u16SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u16, right: u16) -> u16 {\n return Intrinsic.u16SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u16, right: u16) -> u16 {\n return Intrinsic.u16SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u16` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u16` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u16` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u16, right: u16) -> Option {\n return Intrinsic.u16CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u16, right: u16) -> bool {\n return Intrinsic.u16Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u16, right: u16) -> bool {\n return Intrinsic.u16NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u16, right: u16) -> bool {\n return Intrinsic.u16LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u16, right: u16) -> bool {\n return Intrinsic.u16LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u16, right: u16) -> bool {\n return Intrinsic.u16GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u16, right: u16) -> bool {\n return Intrinsic.u16GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u16) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u16`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u16` range.\npub fn parse(text: string) -> Result {\n return Format.u16Value(text)\n}\n', }, { module: 'silk/u32', path: 'silk/u32.silk', sourceIdentity: 'silk/u32', - digest: '4143612fc35767a4a3d45e5335b718a7fcdc2c7d6daeef5729ef17ad43ad7a23', + digest: '286209d110d1a49142e7c8dadbfb6cbb7bdcf6b6a20049e0a65ab4a60912c5f6', documentation: 'silk/u32.silk', layer: 'portable', runtimeInventory: [ @@ -1207,13 +1207,13 @@ export const modules = [ ], namespace: 'u32', source: - '//! Thirty-two-bit unsigned integers for fixed-width counts, masks, and binary fields.\n//!\n//! # When to use\n//! Use `u32` when an unsigned 32-bit representation is part of the contract, including masks and\n//! encoded fields. Use `usize` for collection positions and allocation sizes tied to the target.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`], `wrapping*` computes modulo 2^32, and `saturating*` clamps at\n//! [`MIN`] or [`MAX`]. Right shift is logical and inserts zero bits.\n//!\n//! Decimal [`parse`] requires a complete unsigned representation and reports malformed input\n//! separately from overflow. [`toText`] allocates owned text.\n//!\n//! # Examples\n//! ## Extract one byte from a fixed-width field\n//! ```silk\n//! import silk.u32 as u32\n//!\n//! pub fn main() -> i32 {\n//! let shifted = u32.shiftRight(0x00002A00, 8)\n//! let byte = u32.bitAnd(shifted, 0xFF)\n//! return u32.toI32(byte)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `u32` value.\npub const MAX: u32 = 4294967295\n\n/// The smallest `u32` value.\npub const MIN: u32 = 0\n\n/// The fixed width of `u32`, in bits.\npub const BITS: u32 = 32\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: u32) -> u8 {\n return Intrinsic.u32ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: u32) -> Option {\n return Intrinsic.u32CheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: u32) -> u16 {\n return Intrinsic.u32ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: u32) -> Option {\n return Intrinsic.u32CheckedToU16(value)\n}\n\n/// Returns `value` unchanged as `u32`. Use this function when generic conversion code\n/// can select `u32` as both source and destination.\npub fn toU32(value: u32) -> u32 {\n return Intrinsic.u32ToU32(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u32`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU32(value: u32) -> Option {\n return Intrinsic.u32CheckedToU32(value)\n}\n\n/// Converts `value` exactly to `u64`. Every `u32` value is representable.\npub fn toU64(value: u32) -> u64 {\n return Intrinsic.u32ToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `u32` value is\n/// representable.\npub fn checkedToU64(value: u32) -> Option {\n return Intrinsic.u32CheckedToU64(value)\n}\n\n/// Converts `value` exactly to `usize`. Every `u32` value is representable.\npub fn toUsize(value: u32) -> usize {\n return Intrinsic.u32ToUsize(value)\n}\n\n/// Converts `value` exactly to `usize` and returns `Some`. Every `u32` value is\n/// representable.\npub fn checkedToUsize(value: u32) -> Option {\n return Intrinsic.u32CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u32) -> i8 {\n return Intrinsic.u32ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u32) -> Option {\n return Intrinsic.u32CheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: u32) -> i16 {\n return Intrinsic.u32ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: u32) -> Option {\n return Intrinsic.u32CheckedToI16(value)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: u32) -> i32 {\n return Intrinsic.u32ToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: u32) -> Option {\n return Intrinsic.u32CheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `u32` value is representable.\npub fn toI64(value: u32) -> i64 {\n return Intrinsic.u32ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `u32` value is\n/// representable.\npub fn checkedToI64(value: u32) -> Option {\n return Intrinsic.u32CheckedToI64(value)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: u32) -> isize {\n return Intrinsic.u32ToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: u32) -> Option {\n return Intrinsic.u32CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u32) -> f32 {\n return Intrinsic.u32ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u32) -> f64 {\n return Intrinsic.u32ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u32` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u32` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u32` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u32, right: u32) -> u32 {\n return Intrinsic.u32BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u32, right: u32) -> u32 {\n return Intrinsic.u32BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u32, right: u32) -> u32 {\n return Intrinsic.u32BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u32) -> u32 {\n return Intrinsic.u32BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u32, right: u32) -> u32 {\n return Intrinsic.u32ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u32, right: u32) -> u32 {\n return Intrinsic.u32ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u32, right: u32) -> u32 {\n return Intrinsic.u32RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u32, right: u32) -> u32 {\n return Intrinsic.u32RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u32, right: u32) -> u32 {\n return Intrinsic.u32WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u32, right: u32) -> u32 {\n return Intrinsic.u32WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u32, right: u32) -> u32 {\n return Intrinsic.u32WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u32, right: u32) -> u32 {\n return Intrinsic.u32SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u32, right: u32) -> u32 {\n return Intrinsic.u32SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u32, right: u32) -> u32 {\n return Intrinsic.u32SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u32` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u32` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u32` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u32, right: u32) -> bool {\n return Intrinsic.u32Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u32, right: u32) -> bool {\n return Intrinsic.u32NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u32, right: u32) -> bool {\n return Intrinsic.u32LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u32, right: u32) -> bool {\n return Intrinsic.u32LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u32, right: u32) -> bool {\n return Intrinsic.u32GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u32, right: u32) -> bool {\n return Intrinsic.u32GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u32) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u32`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u32` range.\npub fn parse(text: string) -> Result {\n return Format.u32Value(text)\n}\n', + '//! Thirty-two-bit unsigned integers for fixed-width counts, masks, and binary fields.\n//!\n//! # When to use\n//! Use `u32` when an unsigned 32-bit representation is part of the contract, including masks and\n//! encoded fields. Use `usize` for collection positions and allocation sizes tied to the target.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`], `wrapping*` computes modulo 2^32, and `saturating*` clamps at\n//! [`MIN`] or [`MAX`]. Right shift is logical and inserts zero bits.\n//!\n//! Decimal [`parse`] requires a complete unsigned representation and reports malformed input\n//! separately from overflow. [`toText`] allocates owned text.\n//!\n//! # Examples\n//! ## Extract one byte from a fixed-width field\n//! ```silk\n//! import silk.u32 as u32\n//!\n//! pub fn main() -> i32 {\n//! let shifted = u32.shiftRight(0x00002A00, 8)\n//! let byte = u32.bitAnd(shifted, 0xFF)\n//! return u32.toI32(byte)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u64 as u64\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `u32` value.\npub const MAX: u32 = 4294967295\n\n/// The smallest `u32` value.\npub const MIN: u32 = 0\n\n/// The fixed width of `u32`, in bits.\npub const BITS: u32 = 32\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: u32) -> u8 {\n return Intrinsic.u32ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: u32) -> Option {\n return Intrinsic.u32CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: u32) -> u16 {\n return Intrinsic.u32ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: u32) -> Option {\n return Intrinsic.u32CheckedToU16>(value, some, none)\n}\n\n/// Returns `value` unchanged as `u32`. Use this function when generic conversion code\n/// can select `u32` as both source and destination.\npub fn toU32(value: u32) -> u32 {\n return Intrinsic.u32ToU32(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u32`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU32(value: u32) -> Option {\n return Intrinsic.u32CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` exactly to `u64`. Every `u32` value is representable.\npub fn toU64(value: u32) -> u64 {\n return Intrinsic.u32ToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `u32` value is\n/// representable.\npub fn checkedToU64(value: u32) -> Option {\n return Intrinsic.u32CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` exactly to `usize`. Every `u32` value is representable.\npub fn toUsize(value: u32) -> usize {\n return Intrinsic.u32ToUsize(value)\n}\n\n/// Converts `value` exactly to `usize` and returns `Some`. Every `u32` value is\n/// representable.\npub fn checkedToUsize(value: u32) -> Option {\n return Intrinsic.u32CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u32) -> i8 {\n return Intrinsic.u32ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u32) -> Option {\n return Intrinsic.u32CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: u32) -> i16 {\n return Intrinsic.u32ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: u32) -> Option {\n return Intrinsic.u32CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: u32) -> i32 {\n return Intrinsic.u32ToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: u32) -> Option {\n return Intrinsic.u32CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `u32` value is representable.\npub fn toI64(value: u32) -> i64 {\n return Intrinsic.u32ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `u32` value is\n/// representable.\npub fn checkedToI64(value: u32) -> Option {\n return Intrinsic.u32CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: u32) -> isize {\n return Intrinsic.u32ToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: u32) -> Option {\n return Intrinsic.u32CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u32) -> f32 {\n return Intrinsic.u32ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u32) -> f64 {\n return Intrinsic.u32ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u32` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u32` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u32` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u32, right: u32) -> u32 {\n return Intrinsic.u32Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u32, right: u32) -> u32 {\n return Intrinsic.u32BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u32, right: u32) -> u32 {\n return Intrinsic.u32BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u32, right: u32) -> u32 {\n return Intrinsic.u32BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u32) -> u32 {\n return Intrinsic.u32BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u32, right: u32) -> u32 {\n return Intrinsic.u32ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u32, right: u32) -> u32 {\n return Intrinsic.u32ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u32, right: u32) -> u32 {\n return Intrinsic.u32RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u32, right: u32) -> u32 {\n return Intrinsic.u32RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u32, right: u32) -> u32 {\n return Intrinsic.u32WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u32, right: u32) -> u32 {\n return Intrinsic.u32WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u32` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u32, right: u32) -> u32 {\n return Intrinsic.u32WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u32, right: u32) -> u32 {\n return Intrinsic.u32SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u32, right: u32) -> u32 {\n return Intrinsic.u32SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u32, right: u32) -> u32 {\n return Intrinsic.u32SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u32` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u32` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u32` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u32, right: u32) -> Option {\n return Intrinsic.u32CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u32, right: u32) -> bool {\n return Intrinsic.u32Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u32, right: u32) -> bool {\n return Intrinsic.u32NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u32, right: u32) -> bool {\n return Intrinsic.u32LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u32, right: u32) -> bool {\n return Intrinsic.u32LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u32, right: u32) -> bool {\n return Intrinsic.u32GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u32, right: u32) -> bool {\n return Intrinsic.u32GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u32) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u32`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u32` range.\npub fn parse(text: string) -> Result {\n return Format.u32Value(text)\n}\n', }, { module: 'silk/u64', path: 'silk/u64.silk', sourceIdentity: 'silk/u64', - digest: '2ac2a09c9717556b5a5db75fb85aa29f9f7087b4deeec070d2e076201893b65b', + digest: '043794f6ac2f60cf5fce5a11fac23333e30e3c1277e47e1a0279419e37aa746a', documentation: 'silk/u64.silk', layer: 'portable', runtimeInventory: [ @@ -1272,13 +1272,13 @@ export const modules = [ ], namespace: 'u64', source: - "//! Sixty-four-bit unsigned integers for wide masks, counters, hashes, and exact interchange values.\n//!\n//! # When to use\n//! Use `u64` when a stable unsigned 64-bit representation matters. It is the representation used\n//! for hash output; use `usize` instead for target-sized collection lengths and indices.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`], `wrapping*` computes modulo 2^64, and `saturating*` clamps at\n//! [`MIN`] or [`MAX`]. Right shift inserts zero bits.\n//!\n//! Decimal [`parse`] consumes the complete input and separates malformed text from range overflow.\n//! [`toText`] allocates owned text. Conversion to a floating type may round large values.\n//!\n//! # Gotchas\n//! A `u64` can represent values that neither `i64` nor every target's `usize` can hold; use a\n//! checked conversion when that boundary is controlled by input.\n//!\n//! # Examples\n//! ## Read the high half of a 64-bit field\n//! ```silk\n//! import silk.u64 as u64\n//!\n//! pub fn main() -> i32 {\n//! let high = u64.shiftRight(0x0000002A00000000, 32)\n//! return u64.toI32(high)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `u64` value.\npub const MAX: u64 = 18446744073709551615\n\n/// The smallest `u64` value.\npub const MIN: u64 = 0\n\n/// The fixed width of `u64`, in bits.\npub const BITS: u32 = 64\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: u64) -> u8 {\n return Intrinsic.u64ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: u64) -> Option {\n return Intrinsic.u64CheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: u64) -> u16 {\n return Intrinsic.u64ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: u64) -> Option {\n return Intrinsic.u64CheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: u64) -> u32 {\n return Intrinsic.u64ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: u64) -> Option {\n return Intrinsic.u64CheckedToU32(value)\n}\n\n/// Returns `value` unchanged as `u64`. Use this function when generic conversion code\n/// can select `u64` as both source and destination.\npub fn toU64(value: u64) -> u64 {\n return Intrinsic.u64ToU64(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u64`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU64(value: u64) -> Option {\n return Intrinsic.u64CheckedToU64(value)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: u64) -> usize {\n return Intrinsic.u64ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: u64) -> Option {\n return Intrinsic.u64CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u64) -> i8 {\n return Intrinsic.u64ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u64) -> Option {\n return Intrinsic.u64CheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: u64) -> i16 {\n return Intrinsic.u64ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: u64) -> Option {\n return Intrinsic.u64CheckedToI16(value)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: u64) -> i32 {\n return Intrinsic.u64ToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: u64) -> Option {\n return Intrinsic.u64CheckedToI32(value)\n}\n\n/// Converts `value` to `i64`. Traps if `value` is outside the `i64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI64(value: u64) -> i64 {\n return Intrinsic.u64ToI64(value)\n}\n\n/// Converts `value` to `i64`, or returns `None` if `value` is outside the `i64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI64(value: u64) -> Option {\n return Intrinsic.u64CheckedToI64(value)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: u64) -> isize {\n return Intrinsic.u64ToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: u64) -> Option {\n return Intrinsic.u64CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u64) -> f32 {\n return Intrinsic.u64ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u64) -> f64 {\n return Intrinsic.u64ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u64` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u64` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u64` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u64, right: u64) -> u64 {\n return Intrinsic.u64BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u64, right: u64) -> u64 {\n return Intrinsic.u64BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u64, right: u64) -> u64 {\n return Intrinsic.u64BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u64) -> u64 {\n return Intrinsic.u64BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u64, right: u64) -> u64 {\n return Intrinsic.u64ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u64, right: u64) -> u64 {\n return Intrinsic.u64ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u64, right: u64) -> u64 {\n return Intrinsic.u64RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u64, right: u64) -> u64 {\n return Intrinsic.u64RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u64, right: u64) -> u64 {\n return Intrinsic.u64WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u64, right: u64) -> u64 {\n return Intrinsic.u64WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u64, right: u64) -> u64 {\n return Intrinsic.u64WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u64, right: u64) -> u64 {\n return Intrinsic.u64SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u64, right: u64) -> u64 {\n return Intrinsic.u64SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u64, right: u64) -> u64 {\n return Intrinsic.u64SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u64` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u64` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u64` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u64, right: u64) -> bool {\n return Intrinsic.u64Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u64, right: u64) -> bool {\n return Intrinsic.u64NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u64, right: u64) -> bool {\n return Intrinsic.u64LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u64, right: u64) -> bool {\n return Intrinsic.u64LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u64, right: u64) -> bool {\n return Intrinsic.u64GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u64, right: u64) -> bool {\n return Intrinsic.u64GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u64) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u64`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u64` range.\npub fn parse(text: string) -> Result {\n return Format.u64Value(text)\n}\n", + "//! Sixty-four-bit unsigned integers for wide masks, counters, hashes, and exact interchange values.\n//!\n//! # When to use\n//! Use `u64` when a stable unsigned 64-bit representation matters. It is the representation used\n//! for hash output; use `usize` instead for target-sized collection lengths and indices.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`], `wrapping*` computes modulo 2^64, and `saturating*` clamps at\n//! [`MIN`] or [`MAX`]. Right shift inserts zero bits.\n//!\n//! Decimal [`parse`] consumes the complete input and separates malformed text from range overflow.\n//! [`toText`] allocates owned text. Conversion to a floating type may round large values.\n//!\n//! # Gotchas\n//! A `u64` can represent values that neither `i64` nor every target's `usize` can hold; use a\n//! checked conversion when that boundary is controlled by input.\n//!\n//! # Examples\n//! ## Read the high half of a 64-bit field\n//! ```silk\n//! import silk.u64 as u64\n//!\n//! pub fn main() -> i32 {\n//! let high = u64.shiftRight(0x0000002A00000000, 32)\n//! return u64.toI32(high)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The largest `u64` value.\npub const MAX: u64 = 18446744073709551615\n\n/// The smallest `u64` value.\npub const MIN: u64 = 0\n\n/// The fixed width of `u64`, in bits.\npub const BITS: u32 = 64\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: u64) -> u8 {\n return Intrinsic.u64ToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: u64) -> Option {\n return Intrinsic.u64CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: u64) -> u16 {\n return Intrinsic.u64ToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: u64) -> Option {\n return Intrinsic.u64CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: u64) -> u32 {\n return Intrinsic.u64ToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: u64) -> Option {\n return Intrinsic.u64CheckedToU32>(value, some, none)\n}\n\n/// Returns `value` unchanged as `u64`. Use this function when generic conversion code\n/// can select `u64` as both source and destination.\npub fn toU64(value: u64) -> u64 {\n return Intrinsic.u64ToU64(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u64`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU64(value: u64) -> Option {\n return Intrinsic.u64CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toUsize(value: u64) -> usize {\n return Intrinsic.u64ToUsize(value)\n}\n\n/// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToUsize(value: u64) -> Option {\n return Intrinsic.u64CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u64) -> i8 {\n return Intrinsic.u64ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u64) -> Option {\n return Intrinsic.u64CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: u64) -> i16 {\n return Intrinsic.u64ToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: u64) -> Option {\n return Intrinsic.u64CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: u64) -> i32 {\n return Intrinsic.u64ToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: u64) -> Option {\n return Intrinsic.u64CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` to `i64`. Traps if `value` is outside the `i64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI64(value: u64) -> i64 {\n return Intrinsic.u64ToI64(value)\n}\n\n/// Converts `value` to `i64`, or returns `None` if `value` is outside the `i64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI64(value: u64) -> Option {\n return Intrinsic.u64CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: u64) -> isize {\n return Intrinsic.u64ToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: u64) -> Option {\n return Intrinsic.u64CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u64) -> f32 {\n return Intrinsic.u64ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u64) -> f64 {\n return Intrinsic.u64ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u64` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u64` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u64` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u64, right: u64) -> u64 {\n return Intrinsic.u64Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u64, right: u64) -> u64 {\n return Intrinsic.u64BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u64, right: u64) -> u64 {\n return Intrinsic.u64BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u64, right: u64) -> u64 {\n return Intrinsic.u64BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u64) -> u64 {\n return Intrinsic.u64BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u64, right: u64) -> u64 {\n return Intrinsic.u64ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u64, right: u64) -> u64 {\n return Intrinsic.u64ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u64, right: u64) -> u64 {\n return Intrinsic.u64RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u64, right: u64) -> u64 {\n return Intrinsic.u64RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u64, right: u64) -> u64 {\n return Intrinsic.u64WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u64, right: u64) -> u64 {\n return Intrinsic.u64WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u64` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u64, right: u64) -> u64 {\n return Intrinsic.u64WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u64, right: u64) -> u64 {\n return Intrinsic.u64SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u64, right: u64) -> u64 {\n return Intrinsic.u64SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u64, right: u64) -> u64 {\n return Intrinsic.u64SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u64` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u64` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u64` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u64, right: u64) -> Option {\n return Intrinsic.u64CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u64, right: u64) -> bool {\n return Intrinsic.u64Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u64, right: u64) -> bool {\n return Intrinsic.u64NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u64, right: u64) -> bool {\n return Intrinsic.u64LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u64, right: u64) -> bool {\n return Intrinsic.u64LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u64, right: u64) -> bool {\n return Intrinsic.u64GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u64, right: u64) -> bool {\n return Intrinsic.u64GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u64) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u64`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u64` range.\npub fn parse(text: string) -> Result {\n return Format.u64Value(text)\n}\n", }, { module: 'silk/u8', path: 'silk/u8.silk', sourceIdentity: 'silk/u8', - digest: '47e71b226003a8f69bc668a6e71c4271d2d270b9eddaf790ebefbbc39b9e97a4', + digest: '4c0ec48c470ae6e8669597afd719a6fd928f691f2db45d22efd8a31db82cebc7', documentation: 'silk/u8.silk', layer: 'portable', runtimeInventory: [ @@ -1337,19 +1337,19 @@ export const modules = [ ], namespace: 'u8', source: - '//! Eight-bit unsigned integers for bytes, compact counters, and exact binary representations.\n//!\n//! # When to use\n//! Use `u8` for an individual byte or a field specified as eight unsigned bits. Prefer a wider\n//! integer for general counting unless the 0 through 255 range is intentional.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`] for invalid arithmetic, `wrapping*` computes modulo 2^8, and\n//! `saturating*` clamps at [`MIN`] or [`MAX`]. Right shift inserts zero bits.\n//!\n//! Decimal [`parse`] accepts only a complete unsigned representation; a leading minus sign is\n//! malformed rather than an out-of-range unsigned value. [`toText`] allocates owned text.\n//!\n//! # Examples\n//! ## Choose an overflow policy instead of relying on a trap\n//! ```silk\n//! import silk.option as Option\n//!\n//! import silk.u8 as u8\n//!\n//! pub fn main() -> i32 {\n//! let checked = u8.checkedAdd(255, 1)\n//! let recovered = move checked\n//! |> Option.unwrapOr(42)\n//! if recovered != 42 {\n//! return 1\n//! }\n//! if u8.wrappingAdd(255, 1) != 0 {\n//! return 2\n//! }\n//! if u8.saturatingAdd(255, 1) != 255 {\n//! return 3\n//! }\n//! return 42\n//! }\n//! ```\n//!\n//! # See also\n//! Use the `silk.char` module when a value is a Unicode scalar rather than an arbitrary byte.\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.usize as usize\n\n/// The largest `u8` value.\npub const MAX: u8 = 255\n\n/// The smallest `u8` value.\npub const MIN: u8 = 0\n\n/// The fixed width of `u8`, in bits.\npub const BITS: u32 = 8\n\n/// Returns `value` unchanged as `u8`. Use this function when generic conversion code\n/// can select `u8` as both source and destination.\npub fn toU8(value: u8) -> u8 {\n return Intrinsic.u8ToU8(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u8`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU8(value: u8) -> Option {\n return Intrinsic.u8CheckedToU8(value)\n}\n\n/// Converts `value` exactly to `u16`. Every `u8` value is representable.\npub fn toU16(value: u8) -> u16 {\n return Intrinsic.u8ToU16(value)\n}\n\n/// Converts `value` exactly to `u16` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToU16(value: u8) -> Option {\n return Intrinsic.u8CheckedToU16(value)\n}\n\n/// Converts `value` exactly to `u32`. Every `u8` value is representable.\npub fn toU32(value: u8) -> u32 {\n return Intrinsic.u8ToU32(value)\n}\n\n/// Converts `value` exactly to `u32` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToU32(value: u8) -> Option {\n return Intrinsic.u8CheckedToU32(value)\n}\n\n/// Converts `value` exactly to `u64`. Every `u8` value is representable.\npub fn toU64(value: u8) -> u64 {\n return Intrinsic.u8ToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToU64(value: u8) -> Option {\n return Intrinsic.u8CheckedToU64(value)\n}\n\n/// Converts `value` exactly to `usize`. Every `u8` value is representable.\npub fn toUsize(value: u8) -> usize {\n return Intrinsic.u8ToUsize(value)\n}\n\n/// Converts `value` exactly to `usize` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToUsize(value: u8) -> Option {\n return Intrinsic.u8CheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u8) -> i8 {\n return Intrinsic.u8ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u8) -> Option {\n return Intrinsic.u8CheckedToI8(value)\n}\n\n/// Converts `value` exactly to `i16`. Every `u8` value is representable.\npub fn toI16(value: u8) -> i16 {\n return Intrinsic.u8ToI16(value)\n}\n\n/// Converts `value` exactly to `i16` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToI16(value: u8) -> Option {\n return Intrinsic.u8CheckedToI16(value)\n}\n\n/// Converts `value` exactly to `i32`. Every `u8` value is representable.\npub fn toI32(value: u8) -> i32 {\n return Intrinsic.u8ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToI32(value: u8) -> Option {\n return Intrinsic.u8CheckedToI32(value)\n}\n\n/// Converts `value` exactly to `i64`. Every `u8` value is representable.\npub fn toI64(value: u8) -> i64 {\n return Intrinsic.u8ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToI64(value: u8) -> Option {\n return Intrinsic.u8CheckedToI64(value)\n}\n\n/// Converts `value` exactly to `isize`. Every `u8` value is representable.\npub fn toIsize(value: u8) -> isize {\n return Intrinsic.u8ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToIsize(value: u8) -> Option {\n return Intrinsic.u8CheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u8) -> f32 {\n return Intrinsic.u8ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u8) -> f64 {\n return Intrinsic.u8ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u8` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u8` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u8` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u8, right: u8) -> u8 {\n return Intrinsic.u8BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u8, right: u8) -> u8 {\n return Intrinsic.u8BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u8, right: u8) -> u8 {\n return Intrinsic.u8BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u8) -> u8 {\n return Intrinsic.u8BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u8, right: u8) -> u8 {\n return Intrinsic.u8ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u8, right: u8) -> u8 {\n return Intrinsic.u8ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u8, right: u8) -> u8 {\n return Intrinsic.u8RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u8, right: u8) -> u8 {\n return Intrinsic.u8RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u8, right: u8) -> u8 {\n return Intrinsic.u8WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u8, right: u8) -> u8 {\n return Intrinsic.u8WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u8, right: u8) -> u8 {\n return Intrinsic.u8WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u8, right: u8) -> u8 {\n return Intrinsic.u8SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u8, right: u8) -> u8 {\n return Intrinsic.u8SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u8, right: u8) -> u8 {\n return Intrinsic.u8SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u8` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u8` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u8` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u8, right: u8) -> bool {\n return Intrinsic.u8Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u8, right: u8) -> bool {\n return Intrinsic.u8NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u8, right: u8) -> bool {\n return Intrinsic.u8LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u8, right: u8) -> bool {\n return Intrinsic.u8LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u8, right: u8) -> bool {\n return Intrinsic.u8GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u8, right: u8) -> bool {\n return Intrinsic.u8GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u8) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u8`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u8` range.\npub fn parse(text: string) -> Result {\n return Format.u8Value(text)\n}\n', + '//! Eight-bit unsigned integers for bytes, compact counters, and exact binary representations.\n//!\n//! # When to use\n//! Use `u8` for an individual byte or a field specified as eight unsigned bits. Prefer a wider\n//! integer for general counting unless the 0 through 255 range is intentional.\n//!\n//! # Details\n//! Ordinary arithmetic, narrowing conversions, division by zero, and invalid shift counts trap.\n//! `checked*` returns [`Option`] for invalid arithmetic, `wrapping*` computes modulo 2^8, and\n//! `saturating*` clamps at [`MIN`] or [`MAX`]. Right shift inserts zero bits.\n//!\n//! Decimal [`parse`] accepts only a complete unsigned representation; a leading minus sign is\n//! malformed rather than an out-of-range unsigned value. [`toText`] allocates owned text.\n//!\n//! # Examples\n//! ## Choose an overflow policy instead of relying on a trap\n//! ```silk\n//! import silk.option as Option\n//!\n//! import silk.u8 as u8\n//!\n//! pub fn main() -> i32 {\n//! let checked = u8.checkedAdd(255, 1)\n//! let recovered = move checked\n//! |> Option.unwrapOr(42)\n//! if recovered != 42 {\n//! return 1\n//! }\n//! if u8.wrappingAdd(255, 1) != 0 {\n//! return 2\n//! }\n//! if u8.saturatingAdd(255, 1) != 255 {\n//! return 3\n//! }\n//! return 42\n//! }\n//! ```\n//!\n//! # See also\n//! Use the `silk.char` module when a value is a Unicode scalar rather than an arbitrary byte.\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.usize as usize\n\n/// The largest `u8` value.\npub const MAX: u8 = 255\n\n/// The smallest `u8` value.\npub const MIN: u8 = 0\n\n/// The fixed width of `u8`, in bits.\npub const BITS: u32 = 8\n\n/// Returns `value` unchanged as `u8`. Use this function when generic conversion code\n/// can select `u8` as both source and destination.\npub fn toU8(value: u8) -> u8 {\n return Intrinsic.u8ToU8(value)\n}\n\n/// Returns `Some` with `value` unchanged as `u8`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToU8(value: u8) -> Option {\n return Intrinsic.u8CheckedToU8>(value, some, none)\n}\n\n/// Converts `value` exactly to `u16`. Every `u8` value is representable.\npub fn toU16(value: u8) -> u16 {\n return Intrinsic.u8ToU16(value)\n}\n\n/// Converts `value` exactly to `u16` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToU16(value: u8) -> Option {\n return Intrinsic.u8CheckedToU16>(value, some, none)\n}\n\n/// Converts `value` exactly to `u32`. Every `u8` value is representable.\npub fn toU32(value: u8) -> u32 {\n return Intrinsic.u8ToU32(value)\n}\n\n/// Converts `value` exactly to `u32` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToU32(value: u8) -> Option {\n return Intrinsic.u8CheckedToU32>(value, some, none)\n}\n\n/// Converts `value` exactly to `u64`. Every `u8` value is representable.\npub fn toU64(value: u8) -> u64 {\n return Intrinsic.u8ToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToU64(value: u8) -> Option {\n return Intrinsic.u8CheckedToU64>(value, some, none)\n}\n\n/// Converts `value` exactly to `usize`. Every `u8` value is representable.\npub fn toUsize(value: u8) -> usize {\n return Intrinsic.u8ToUsize(value)\n}\n\n/// Converts `value` exactly to `usize` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToUsize(value: u8) -> Option {\n return Intrinsic.u8CheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: u8) -> i8 {\n return Intrinsic.u8ToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: u8) -> Option {\n return Intrinsic.u8CheckedToI8>(value, some, none)\n}\n\n/// Converts `value` exactly to `i16`. Every `u8` value is representable.\npub fn toI16(value: u8) -> i16 {\n return Intrinsic.u8ToI16(value)\n}\n\n/// Converts `value` exactly to `i16` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToI16(value: u8) -> Option {\n return Intrinsic.u8CheckedToI16>(value, some, none)\n}\n\n/// Converts `value` exactly to `i32`. Every `u8` value is representable.\npub fn toI32(value: u8) -> i32 {\n return Intrinsic.u8ToI32(value)\n}\n\n/// Converts `value` exactly to `i32` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToI32(value: u8) -> Option {\n return Intrinsic.u8CheckedToI32>(value, some, none)\n}\n\n/// Converts `value` exactly to `i64`. Every `u8` value is representable.\npub fn toI64(value: u8) -> i64 {\n return Intrinsic.u8ToI64(value)\n}\n\n/// Converts `value` exactly to `i64` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToI64(value: u8) -> Option {\n return Intrinsic.u8CheckedToI64>(value, some, none)\n}\n\n/// Converts `value` exactly to `isize`. Every `u8` value is representable.\npub fn toIsize(value: u8) -> isize {\n return Intrinsic.u8ToIsize(value)\n}\n\n/// Converts `value` exactly to `isize` and returns `Some`. Every `u8` value is\n/// representable.\npub fn checkedToIsize(value: u8) -> Option {\n return Intrinsic.u8CheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: u8) -> f32 {\n return Intrinsic.u8ToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: u8) -> f64 {\n return Intrinsic.u8ToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `u8` range. Use this function\n/// when overflow is a program error.\npub fn add(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Add(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `u8` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Subtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `u8` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Multiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Divide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: u8, right: u8) -> u8 {\n return Intrinsic.u8Remainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: u8, right: u8) -> u8 {\n return Intrinsic.u8BitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: u8, right: u8) -> u8 {\n return Intrinsic.u8BitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: u8, right: u8) -> u8 {\n return Intrinsic.u8BitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: u8) -> u8 {\n return Intrinsic.u8BitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: u8, right: u8) -> u8 {\n return Intrinsic.u8ShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: u8, right: u8) -> u8 {\n return Intrinsic.u8ShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: u8, right: u8) -> u8 {\n return Intrinsic.u8RotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: u8, right: u8) -> u8 {\n return Intrinsic.u8RotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `u8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: u8, right: u8) -> u8 {\n return Intrinsic.u8WrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `u8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: u8, right: u8) -> u8 {\n return Intrinsic.u8WrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `u8` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: u8, right: u8) -> u8 {\n return Intrinsic.u8WrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: u8, right: u8) -> u8 {\n return Intrinsic.u8SaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: u8, right: u8) -> u8 {\n return Intrinsic.u8SaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: u8, right: u8) -> u8 {\n return Intrinsic.u8SaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `u8` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `u8` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `u8` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: u8, right: u8) -> Option {\n return Intrinsic.u8CheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: u8, right: u8) -> bool {\n return Intrinsic.u8Equals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: u8, right: u8) -> bool {\n return Intrinsic.u8NotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: u8, right: u8) -> bool {\n return Intrinsic.u8LessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: u8, right: u8) -> bool {\n return Intrinsic.u8LessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: u8, right: u8) -> bool {\n return Intrinsic.u8GreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: u8, right: u8) -> bool {\n return Intrinsic.u8GreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: u8) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `u8`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` if the number is outside the `u8` range.\npub fn parse(text: string) -> Result {\n return Format.u8Value(text)\n}\n', }, { module: 'silk/unicode', path: 'silk/unicode.silk', sourceIdentity: 'silk/unicode', - digest: '16934b2629170e9b31c0df0479ba1a817f6a8879dd3c1d851bdf717ef3442ff8', + digest: '833c20a136e07d3c15cb4981eb77e26e8db02bf39cd02fe134882ec20b6d8753', documentation: 'silk/unicode.silk', layer: 'portable', runtimeInventory: [], namespace: 'Unicode', source: - '//! Explicit Unicode canonical normalization backed by the pinned Unicode 17.0.0 data set.\n//!\n//! # When to use\n//! Normalize text at a boundary where canonically equivalent spellings must compare alike. Use\n//! [`normalizeNfc`] for ordinary storage and comparison, or [`normalizeNfd`] when a decomposed,\n//! canonically ordered sequence is the desired representation.\n//!\n//! # Details\n//! Normalization is opt-in: `string` equality continues to compare exact UTF-8 bytes. Both forms\n//! allocate a fresh owned [`String`], handle Hangul composition algorithmically, and use the same\n//! generated tables on every target. [`dataVersion`] reports which Unicode database defines those\n//! results.\n//!\n//! # Gotchas\n//! This module implements canonical NFC and NFD, not compatibility normalization (NFKC or NFKD),\n//! locale-sensitive comparison, grapheme segmentation, or case folding.\n//!\n//! # Examples\n//! ## Make canonically equivalent text compare equal\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.string as String\n//!\n//! import silk.unicode as Unicode\n//!\n//! effect fn normalize() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let composing = Unicode.normalizeNfc("e\\u{301}")\n//! |> Effect.provideMut(&mut allocator)\n//! let composed = run composing\n//! let decomposing = Unicode.normalizeNfd("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let decomposed = run decomposing\n//! if String.view(&composed) != "é" {\n//! return 0\n//! }\n//! if String.view(&decomposed) != "e\\u{301}" {\n//! return 0\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(normalize(), recover)\n//! }\n//! ```\n\n\n// Unicode policy lives here rather than in the compiler, and it is explicitly invoked. Nothing on\n// this path runs unless a program calls it: ordinary `string` equality still compares exact bytes,\n// so a precomposed `é` and a decomposed `é` stay unequal until source normalizes them.\n//\n// The data these algorithms read is generated into `silk/unicode_tables` from a pinned Unicode\n// database. A later Unicode version regenerates that module and nothing else: no compiler type\n// identity and no target ABI mentions it.\n//\n// The scalar buffers move through the helpers rather than being borrowed into them, because a\n// borrow has to come from a direct binding and a reference parameter cannot be reborrowed. Moving\n// a Vector is a move of its three fields, not a copy of its storage, so the buffer is built once\n// and handed along.\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.result { Result, Success, Failure }\nimport silk.string { String, InvalidUtf8, copyUtf8, make as stringMake, utf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.unicode_tables {\n Decomposition,\n canonicalComposition,\n canonicalDecomposition,\n combiningClass,\n dataVersion as tableDataVersion,\n maximumDecompositionLength\n}\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n make as vectorMake,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n get as vectorGet,\n length as vectorLength,\n set as vectorSet,\n truncate as vectorTruncate\n}\n\n/// Hangul syllables decompose and compose arithmetically rather than through a table, so UAX #15\n/// names these constants instead of tabulating eleven thousand rows.\nconst HANGUL_SYLLABLE_BASE: u32 = 44032\nconst HANGUL_LEADING_BASE: u32 = 4352\nconst HANGUL_VOWEL_BASE: u32 = 4449\nconst HANGUL_TRAILING_BASE: u32 = 4519\nconst HANGUL_LEADING_COUNT: u32 = 19\nconst HANGUL_VOWEL_COUNT: u32 = 21\nconst HANGUL_TRAILING_COUNT: u32 = 28\nconst HANGUL_SYLLABLE_COUNT: u32 = 11172\n\n/// A combining class no scalar carries. Composition uses it for "the starter is still untouched".\nconst NO_COMBINING_CLASS: u32 = 256\n\n/// Room for the trailing components a full decomposition can stack up.\n///\n/// A decomposition recurses through its first component only, so the stack holds one fewer entry\n/// than the longest full decomposition. `namesLongestDecomposition` in the test suite pins the\n/// generated bound against this constant, so a Unicode version that decomposes more deeply fails\n/// loudly rather than silently overrunning.\nconst PENDING_CAPACITY: usize = 4\n\n/// Returns the Unicode version that defines this module\'s normalization results.\n///\n/// # Details\n///\n/// The version is data, not identity: a later database changes what this returns and what the\n/// tables contain, and changes neither a compiler type nor a target ABI.\npub fn dataVersion() -> string {\n return tableDataVersion()\n}\n\n/// Returns the longest full canonical decomposition of one scalar in the active Unicode data.\npub fn longestDecomposition() -> usize {\n return maximumDecompositionLength()\n}\n\n/// Returns a Unicode scalar\'s canonical combining class, or zero for a starter or unknown value.\npub fn canonicalCombiningClass(\n /// Unicode scalar value represented as its unsigned code point.\n scalar: u32,\n) -> u32 {\n return combiningClass(scalar)\n}\n\nfn isHangulSyllable(scalar: u32) -> bool {\n if scalar < HANGUL_SYLLABLE_BASE { return false }\n return scalar < HANGUL_SYLLABLE_BASE + HANGUL_SYLLABLE_COUNT\n}\n\n/// Appends one scalar, then moves it back past any preceding mark of a higher combining class.\n///\n/// Canonical ordering is a stable sort by combining class. Doing it as each scalar arrives keeps\n/// the buffer behind the cursor always in order, so no separate ordering pass is needed.\neffect fn appendOrdered(\n buffer: Vector,\n scalar: u32,\n) -> Vector ! OutOfMemoryError ? &mut Allocator {\n let mut ordered = move buffer\n let appended = run vectorAppend(&mut ordered, scalar)\n let class = combiningClass(scalar)\n if class == 0 { return move ordered }\n let mut index = vectorLength(&ordered) - usize.ONE\n while usize.ZERO < index {\n let previous = vectorGet(&ordered, index - usize.ONE)\n let previousClass = combiningClass(previous)\n if previousClass == 0 { return move ordered }\n if previousClass <= class { return move ordered }\n vectorSet(&mut ordered, index - usize.ONE, scalar)\n vectorSet(&mut ordered, index, previous)\n index = index - usize.ONE\n }\n return move ordered\n}\n\n/// Appends a Hangul syllable\'s canonical decomposition, which is arithmetic rather than tabulated.\neffect fn decomposeHangul(\n buffer: Vector,\n scalar: u32,\n) -> Vector ! OutOfMemoryError ? &mut Allocator {\n let index = scalar - HANGUL_SYLLABLE_BASE\n let leading = HANGUL_LEADING_BASE + index / (HANGUL_VOWEL_COUNT * HANGUL_TRAILING_COUNT)\n let vowel = HANGUL_VOWEL_BASE\n + (index % (HANGUL_VOWEL_COUNT * HANGUL_TRAILING_COUNT)) / HANGUL_TRAILING_COUNT\n let trailing = index % HANGUL_TRAILING_COUNT\n let withLeading = run appendOrdered(move buffer, leading)\n let withVowel = run appendOrdered(move withLeading, vowel)\n if trailing == 0 { return move withVowel }\n return run appendOrdered(move withVowel, HANGUL_TRAILING_BASE + trailing)\n}\n\n/// Appends one scalar\'s full canonical decomposition in canonical order.\n///\n/// Decomposition recurses through a mapping\'s first component only. The generator proves no second\n/// component decomposes further, so the trailing components collect on a small stack and unwind\n/// once the innermost starter is emitted.\neffect fn decomposeInto(\n buffer: Vector,\n scalar: u32,\n) -> Vector ! OutOfMemoryError ? &mut Allocator {\n if isHangulSyllable(scalar) { return run decomposeHangul(move buffer, scalar) }\n let mut pending = [scalar, scalar, scalar, scalar]\n let mut depth = usize.ZERO\n let mut current = scalar\n while true {\n let mapping = canonicalDecomposition(current)\n if mapping.first == 0 { break }\n if mapping.second != 0 {\n pending[depth] = mapping.second\n depth = depth + usize.ONE\n }\n current = mapping.first\n }\n let mut decomposed = run appendOrdered(move buffer, current)\n while usize.ZERO < depth {\n depth = depth - usize.ONE\n let extended = run appendOrdered(move decomposed, pending[depth])\n decomposed = move extended\n }\n return move decomposed\n}\n\n/// Composes a starter with the scalar following it, or returns zero when they do not compose.\nfn composePair(starter: u32, following: u32) -> u32 {\n if HANGUL_LEADING_BASE <= starter {\n if starter < HANGUL_LEADING_BASE + HANGUL_LEADING_COUNT {\n if HANGUL_VOWEL_BASE <= following {\n if following < HANGUL_VOWEL_BASE + HANGUL_VOWEL_COUNT {\n let leading = starter - HANGUL_LEADING_BASE\n let vowel = following - HANGUL_VOWEL_BASE\n return HANGUL_SYLLABLE_BASE\n + (leading * HANGUL_VOWEL_COUNT + vowel) * HANGUL_TRAILING_COUNT\n }\n }\n }\n }\n if isHangulSyllable(starter) {\n if (starter - HANGUL_SYLLABLE_BASE) % HANGUL_TRAILING_COUNT == 0 {\n if HANGUL_TRAILING_BASE < following {\n if following < HANGUL_TRAILING_BASE + HANGUL_TRAILING_COUNT {\n return starter + (following - HANGUL_TRAILING_BASE)\n }\n }\n }\n }\n return canonicalComposition(starter, following)\n}\n\n/// Rewrites a canonically ordered buffer as its canonical composition.\n///\n/// This is the UAX #15 composition pass. A starter absorbs each following scalar it composes with,\n/// and a scalar blocked from its starter by an equal or higher combining class is kept as written.\nfn composeInPlace(buffer: Vector) -> Vector {\n let mut composed = move buffer\n if vectorLength(&composed) == usize.ZERO { return move composed }\n let mut starterIndex = usize.ZERO\n let mut starter = vectorGet(&composed, usize.ZERO)\n let mut written = usize.ONE\n let mut lastClass = combiningClass(starter)\n if lastClass != 0 { lastClass = NO_COMBINING_CLASS }\n let mut index = usize.ONE\n while index < vectorLength(&composed) {\n let scalar = vectorGet(&composed, index)\n let class = combiningClass(scalar)\n let composite = composePair(starter, scalar)\n let mut absorbed = false\n if composite != 0 {\n if lastClass < class { absorbed = true }\n if lastClass == 0 { absorbed = true }\n }\n if absorbed {\n vectorSet(&mut composed, starterIndex, composite)\n starter = composite\n } else {\n if class == 0 {\n starterIndex = written\n starter = scalar\n }\n lastClass = class\n vectorSet(&mut composed, written, scalar)\n written = written + usize.ONE\n }\n index = index + usize.ONE\n }\n vectorTruncate(&mut composed, written)\n return move composed\n}\n\n/// Appends one scalar\'s UTF-8 encoding to a buffer the caller owns directly.\n///\n/// This is written against a local binding rather than as a helper taking the buffer, because a\n/// reference parameter cannot be reborrowed and threading the buffer through by move hit a native\n/// backend defect: a vector rebound by `buffer = move helper(move buffer)` inside a loop reads\n/// stale once the loop ends and the buffer is borrowed. See the note in the pull request.\n/// Decodes valid UTF-8 into scalars, decomposing and canonically ordering as it goes.\n///\n/// The input is a `string`, so it is already valid UTF-8 and the decode needs no validation; the\n/// safe validating decoder is `silk/string`\'s `fromUtf8`.\neffect fn decomposedScalars(value: string) -> Vector ! OutOfMemoryError ? &mut Allocator {\n let bytes = utf8Bytes(value)\n let mut buffer = vectorMake()\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let first = u8.toU32(bytes[offset])\n let mut scalar = first\n let mut width = usize.ONE\n if first > 193 {\n if first <= 223 {\n scalar = (first - 192) * 64 + u8.toU32(bytes[offset + usize.ONE]) - 128\n width = 2\n } else {\n if first <= 239 {\n scalar = (first - 224) * 4096\n + (u8.toU32(bytes[offset + usize.ONE]) - 128) * 64\n + u8.toU32(bytes[offset + 2]) - 128\n width = 3\n } else {\n scalar = (first - 240) * 262144\n + (u8.toU32(bytes[offset + usize.ONE]) - 128) * 4096\n + (u8.toU32(bytes[offset + 2]) - 128) * 64\n + u8.toU32(bytes[offset + 3]) - 128\n width = 4\n }\n }\n }\n let extended = run decomposeInto(move buffer, scalar)\n buffer = move extended\n offset = offset + width\n }\n return move buffer\n}\n\n/// Copies a scalar sequence into owned text.\n///\n/// The encoder only ever writes well-formed UTF-8, so the validation inside `copyUtf8` cannot fail\n/// here; the failing arm returns empty text because this module has no reason to reach for the\n/// unchecked constructor to save a scan it is not paying much for.\neffect fn encodeOwned(scalars: Vector) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = move scalars\n let mut bytes = vectorMake()\n let mut index = usize.ZERO\n while index < vectorLength(&source) {\n let scalar = vectorGet(&source, index)\n if scalar < 128 {\n let only = run vectorAppend(&mut bytes, u32.toU8(scalar))\n } else {\n if scalar < 2048 {\n let lead = run vectorAppend(&mut bytes, u32.toU8(192 + scalar / 64))\n let last = run vectorAppend(&mut bytes, u32.toU8(128 + scalar % 64))\n } else {\n if scalar < 65536 {\n let lead = run vectorAppend(&mut bytes, u32.toU8(224 + scalar / 4096))\n let middle = run vectorAppend(&mut bytes, u32.toU8(128 + (scalar / 64) % 64))\n let last = run vectorAppend(&mut bytes, u32.toU8(128 + scalar % 64))\n } else {\n let lead = run vectorAppend(&mut bytes, u32.toU8(240 + scalar / 262144))\n let second = run vectorAppend(&mut bytes, u32.toU8(128 + (scalar / 4096) % 64))\n let third = run vectorAppend(&mut bytes, u32.toU8(128 + (scalar / 64) % 64))\n let last = run vectorAppend(&mut bytes, u32.toU8(128 + scalar % 64))\n }\n }\n }\n index = index + usize.ONE\n }\n let owned = run copyUtf8(vectorAsSlice(&bytes))\n return match move owned {\n Result { value: outcome } => match move outcome {\n Success { value } => move value\n Failure { error } => stringMake()\n }\n }\n}\n\n/// Returns the Normalization Form D of text: fully decomposed, in canonical order.\n///\n/// # When to use\n///\n/// Use this function when consumers require decomposed scalars in canonical combining-class order.\n/// Use [`normalizeNfc`] for ordinary normalized storage and comparison.\n///\n/// # Details\n///\n/// The function does not change `value`. It returns freshly owned UTF-8 text and can allocate.\n/// Canonically equivalent inputs produce equal NFD text under the same Unicode data version.\npub effect fn normalizeNfd(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let scalars = run decomposedScalars(value)\n return run encodeOwned(move scalars)\n}\n\n/// Returns the Normalization Form C of text: decomposed, canonically ordered, then recomposed.\n///\n/// # When to use\n///\n/// Use this function for ordinary normalized storage and canonical-equivalence comparison. Use\n/// [`normalizeNfd`] when a consumer requires decomposed scalars.\n///\n/// # Details\n///\n/// The function does not change `value`. It returns freshly owned UTF-8 text and can allocate.\n/// Canonically equivalent inputs produce equal NFC text under the same Unicode data version.\npub effect fn normalizeNfc(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let scalars = run decomposedScalars(value)\n let composed = composeInPlace(move scalars)\n return run encodeOwned(move composed)\n}\n', + '//! Explicit Unicode canonical normalization backed by the pinned Unicode 17.0.0 data set.\n//!\n//! # When to use\n//! Normalize text at a boundary where canonically equivalent spellings must compare alike. Use\n//! [`normalizeNfc`] for ordinary storage and comparison, or [`normalizeNfd`] when a decomposed,\n//! canonically ordered sequence is the desired representation.\n//!\n//! # Details\n//! Normalization is opt-in: `string` equality continues to compare exact UTF-8 bytes. Both forms\n//! allocate a fresh owned [`String`], handle Hangul composition algorithmically, and use the same\n//! generated tables on every target. [`dataVersion`] reports which Unicode database defines those\n//! results.\n//!\n//! # Gotchas\n//! This module implements canonical NFC and NFD, not compatibility normalization (NFKC or NFKD),\n//! locale-sensitive comparison, grapheme segmentation, or case folding.\n//!\n//! # Examples\n//! ## Make canonically equivalent text compare equal\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.string as String\n//!\n//! import silk.unicode as Unicode\n//!\n//! effect fn normalize() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let composing = Unicode.normalizeNfc("e\\u{301}")\n//! |> Effect.provideMut(&mut allocator)\n//! let composed = run composing\n//! let decomposing = Unicode.normalizeNfd("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let decomposed = run decomposing\n//! if String.view(&composed) != "é" {\n//! return 0\n//! }\n//! if String.view(&decomposed) != "e\\u{301}" {\n//! return 0\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(normalize(), recover)\n//! }\n//! ```\n\n\n// Unicode policy lives here rather than in the compiler, and it is explicitly invoked. Nothing on\n// this path runs unless a program calls it: ordinary `string` equality still compares exact bytes,\n// so a precomposed `é` and a decomposed `é` stay unequal until source normalizes them.\n//\n// The data these algorithms read is generated into `silk/unicode_tables` from a pinned Unicode\n// database. A later Unicode version regenerates that module and nothing else: no compiler type\n// identity and no target ABI mentions it.\n//\n// The scalar buffers move through the helpers rather than being borrowed into them, because a\n// borrow has to come from a direct binding and a reference parameter cannot be reborrowed. Moving\n// a Vector is a move of its three fields, not a copy of its storage, so the buffer is built once\n// and handed along.\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.result { Result }\nimport silk.string { String, InvalidUtf8, copyUtf8, make as stringMake, utf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.unicode_tables {\n Decomposition,\n canonicalComposition,\n canonicalDecomposition,\n combiningClass,\n dataVersion as tableDataVersion,\n maximumDecompositionLength\n}\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n make as vectorMake,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n get as vectorGet,\n length as vectorLength,\n set as vectorSet,\n truncate as vectorTruncate\n}\n\n/// Hangul syllables decompose and compose arithmetically rather than through a table, so UAX #15\n/// names these constants instead of tabulating eleven thousand rows.\nconst HANGUL_SYLLABLE_BASE: u32 = 44032\nconst HANGUL_LEADING_BASE: u32 = 4352\nconst HANGUL_VOWEL_BASE: u32 = 4449\nconst HANGUL_TRAILING_BASE: u32 = 4519\nconst HANGUL_LEADING_COUNT: u32 = 19\nconst HANGUL_VOWEL_COUNT: u32 = 21\nconst HANGUL_TRAILING_COUNT: u32 = 28\nconst HANGUL_SYLLABLE_COUNT: u32 = 11172\n\n/// A combining class no scalar carries. Composition uses it for "the starter is still untouched".\nconst NO_COMBINING_CLASS: u32 = 256\n\n/// Room for the trailing components a full decomposition can stack up.\n///\n/// A decomposition recurses through its first component only, so the stack holds one fewer entry\n/// than the longest full decomposition. `namesLongestDecomposition` in the test suite pins the\n/// generated bound against this constant, so a Unicode version that decomposes more deeply fails\n/// loudly rather than silently overrunning.\nconst PENDING_CAPACITY: usize = 4\n\n/// Returns the Unicode version that defines this module\'s normalization results.\n///\n/// # Details\n///\n/// The version is data, not identity: a later database changes what this returns and what the\n/// tables contain, and changes neither a compiler type nor a target ABI.\npub fn dataVersion() -> string {\n return tableDataVersion()\n}\n\n/// Returns the longest full canonical decomposition of one scalar in the active Unicode data.\npub fn longestDecomposition() -> usize {\n return maximumDecompositionLength()\n}\n\n/// Returns a Unicode scalar\'s canonical combining class, or zero for a starter or unknown value.\npub fn canonicalCombiningClass(\n /// Unicode scalar value represented as its unsigned code point.\n scalar: u32,\n) -> u32 {\n return combiningClass(scalar)\n}\n\nfn isHangulSyllable(scalar: u32) -> bool {\n if scalar < HANGUL_SYLLABLE_BASE { return false }\n return scalar < HANGUL_SYLLABLE_BASE + HANGUL_SYLLABLE_COUNT\n}\n\n/// Appends one scalar, then moves it back past any preceding mark of a higher combining class.\n///\n/// Canonical ordering is a stable sort by combining class. Doing it as each scalar arrives keeps\n/// the buffer behind the cursor always in order, so no separate ordering pass is needed.\neffect fn appendOrdered(\n buffer: Vector,\n scalar: u32,\n) -> Vector ! OutOfMemoryError ? &mut Allocator {\n let mut ordered = move buffer\n let appended = run vectorAppend(&mut ordered, scalar)\n let class = combiningClass(scalar)\n if class == 0 { return move ordered }\n let mut index = vectorLength(&ordered) - usize.ONE\n while usize.ZERO < index {\n let previous = vectorGet(&ordered, index - usize.ONE)\n let previousClass = combiningClass(previous)\n if previousClass == 0 { return move ordered }\n if previousClass <= class { return move ordered }\n vectorSet(&mut ordered, index - usize.ONE, scalar)\n vectorSet(&mut ordered, index, previous)\n index = index - usize.ONE\n }\n return move ordered\n}\n\n/// Appends a Hangul syllable\'s canonical decomposition, which is arithmetic rather than tabulated.\neffect fn decomposeHangul(\n buffer: Vector,\n scalar: u32,\n) -> Vector ! OutOfMemoryError ? &mut Allocator {\n let index = scalar - HANGUL_SYLLABLE_BASE\n let leading = HANGUL_LEADING_BASE + index / (HANGUL_VOWEL_COUNT * HANGUL_TRAILING_COUNT)\n let vowel = HANGUL_VOWEL_BASE\n + (index % (HANGUL_VOWEL_COUNT * HANGUL_TRAILING_COUNT)) / HANGUL_TRAILING_COUNT\n let trailing = index % HANGUL_TRAILING_COUNT\n let withLeading = run appendOrdered(move buffer, leading)\n let withVowel = run appendOrdered(move withLeading, vowel)\n if trailing == 0 { return move withVowel }\n return run appendOrdered(move withVowel, HANGUL_TRAILING_BASE + trailing)\n}\n\n/// Appends one scalar\'s full canonical decomposition in canonical order.\n///\n/// Decomposition recurses through a mapping\'s first component only. The generator proves no second\n/// component decomposes further, so the trailing components collect on a small stack and unwind\n/// once the innermost starter is emitted.\neffect fn decomposeInto(\n buffer: Vector,\n scalar: u32,\n) -> Vector ! OutOfMemoryError ? &mut Allocator {\n if isHangulSyllable(scalar) { return run decomposeHangul(move buffer, scalar) }\n let mut pending = [scalar, scalar, scalar, scalar]\n let mut depth = usize.ZERO\n let mut current = scalar\n while true {\n let mapping = canonicalDecomposition(current)\n if mapping.first == 0 { break }\n if mapping.second != 0 {\n pending[depth] = mapping.second\n depth = depth + usize.ONE\n }\n current = mapping.first\n }\n let mut decomposed = run appendOrdered(move buffer, current)\n while usize.ZERO < depth {\n depth = depth - usize.ONE\n let extended = run appendOrdered(move decomposed, pending[depth])\n decomposed = move extended\n }\n return move decomposed\n}\n\n/// Composes a starter with the scalar following it, or returns zero when they do not compose.\nfn composePair(starter: u32, following: u32) -> u32 {\n if HANGUL_LEADING_BASE <= starter {\n if starter < HANGUL_LEADING_BASE + HANGUL_LEADING_COUNT {\n if HANGUL_VOWEL_BASE <= following {\n if following < HANGUL_VOWEL_BASE + HANGUL_VOWEL_COUNT {\n let leading = starter - HANGUL_LEADING_BASE\n let vowel = following - HANGUL_VOWEL_BASE\n return HANGUL_SYLLABLE_BASE\n + (leading * HANGUL_VOWEL_COUNT + vowel) * HANGUL_TRAILING_COUNT\n }\n }\n }\n }\n if isHangulSyllable(starter) {\n if (starter - HANGUL_SYLLABLE_BASE) % HANGUL_TRAILING_COUNT == 0 {\n if HANGUL_TRAILING_BASE < following {\n if following < HANGUL_TRAILING_BASE + HANGUL_TRAILING_COUNT {\n return starter + (following - HANGUL_TRAILING_BASE)\n }\n }\n }\n }\n return canonicalComposition(starter, following)\n}\n\n/// Rewrites a canonically ordered buffer as its canonical composition.\n///\n/// This is the UAX #15 composition pass. A starter absorbs each following scalar it composes with,\n/// and a scalar blocked from its starter by an equal or higher combining class is kept as written.\nfn composeInPlace(buffer: Vector) -> Vector {\n let mut composed = move buffer\n if vectorLength(&composed) == usize.ZERO { return move composed }\n let mut starterIndex = usize.ZERO\n let mut starter = vectorGet(&composed, usize.ZERO)\n let mut written = usize.ONE\n let mut lastClass = combiningClass(starter)\n if lastClass != 0 { lastClass = NO_COMBINING_CLASS }\n let mut index = usize.ONE\n while index < vectorLength(&composed) {\n let scalar = vectorGet(&composed, index)\n let class = combiningClass(scalar)\n let composite = composePair(starter, scalar)\n let mut absorbed = false\n if composite != 0 {\n if lastClass < class { absorbed = true }\n if lastClass == 0 { absorbed = true }\n }\n if absorbed {\n vectorSet(&mut composed, starterIndex, composite)\n starter = composite\n } else {\n if class == 0 {\n starterIndex = written\n starter = scalar\n }\n lastClass = class\n vectorSet(&mut composed, written, scalar)\n written = written + usize.ONE\n }\n index = index + usize.ONE\n }\n vectorTruncate(&mut composed, written)\n return move composed\n}\n\n/// Appends one scalar\'s UTF-8 encoding to a buffer the caller owns directly.\n///\n/// This is written against a local binding rather than as a helper taking the buffer, because a\n/// reference parameter cannot be reborrowed and threading the buffer through by move hit a native\n/// backend defect: a vector rebound by `buffer = move helper(move buffer)` inside a loop reads\n/// stale once the loop ends and the buffer is borrowed. See the note in the pull request.\n/// Decodes valid UTF-8 into scalars, decomposing and canonically ordering as it goes.\n///\n/// The input is a `string`, so it is already valid UTF-8 and the decode needs no validation; the\n/// safe validating decoder is `silk/string`\'s `fromUtf8`.\neffect fn decomposedScalars(value: string) -> Vector ! OutOfMemoryError ? &mut Allocator {\n let bytes = utf8Bytes(value)\n let mut buffer = vectorMake()\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let first = u8.toU32(bytes[offset])\n let mut scalar = first\n let mut width = usize.ONE\n if first > 193 {\n if first <= 223 {\n scalar = (first - 192) * 64 + u8.toU32(bytes[offset + usize.ONE]) - 128\n width = 2\n } else {\n if first <= 239 {\n scalar = (first - 224) * 4096\n + (u8.toU32(bytes[offset + usize.ONE]) - 128) * 64\n + u8.toU32(bytes[offset + 2]) - 128\n width = 3\n } else {\n scalar = (first - 240) * 262144\n + (u8.toU32(bytes[offset + usize.ONE]) - 128) * 4096\n + (u8.toU32(bytes[offset + 2]) - 128) * 64\n + u8.toU32(bytes[offset + 3]) - 128\n width = 4\n }\n }\n }\n let extended = run decomposeInto(move buffer, scalar)\n buffer = move extended\n offset = offset + width\n }\n return move buffer\n}\n\n/// Copies a scalar sequence into owned text.\n///\n/// The encoder only ever writes well-formed UTF-8, so the validation inside `copyUtf8` cannot fail\n/// here; the failing arm returns empty text because this module has no reason to reach for the\n/// unchecked constructor to save a scan it is not paying much for.\neffect fn encodeOwned(scalars: Vector) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = move scalars\n let mut bytes = vectorMake()\n let mut index = usize.ZERO\n while index < vectorLength(&source) {\n let scalar = vectorGet(&source, index)\n if scalar < 128 {\n let only = run vectorAppend(&mut bytes, u32.toU8(scalar))\n } else {\n if scalar < 2048 {\n let lead = run vectorAppend(&mut bytes, u32.toU8(192 + scalar / 64))\n let last = run vectorAppend(&mut bytes, u32.toU8(128 + scalar % 64))\n } else {\n if scalar < 65536 {\n let lead = run vectorAppend(&mut bytes, u32.toU8(224 + scalar / 4096))\n let middle = run vectorAppend(&mut bytes, u32.toU8(128 + (scalar / 64) % 64))\n let last = run vectorAppend(&mut bytes, u32.toU8(128 + scalar % 64))\n } else {\n let lead = run vectorAppend(&mut bytes, u32.toU8(240 + scalar / 262144))\n let second = run vectorAppend(&mut bytes, u32.toU8(128 + (scalar / 4096) % 64))\n let third = run vectorAppend(&mut bytes, u32.toU8(128 + (scalar / 64) % 64))\n let last = run vectorAppend(&mut bytes, u32.toU8(128 + scalar % 64))\n }\n }\n }\n index = index + usize.ONE\n }\n let owned = run copyUtf8(vectorAsSlice(&bytes))\n return match move owned {\n Result.Success { value } => move value\n Result.Failure { error } => stringMake()\n }\n}\n\n/// Returns the Normalization Form D of text: fully decomposed, in canonical order.\n///\n/// # When to use\n///\n/// Use this function when consumers require decomposed scalars in canonical combining-class order.\n/// Use [`normalizeNfc`] for ordinary normalized storage and comparison.\n///\n/// # Details\n///\n/// The function does not change `value`. It returns freshly owned UTF-8 text and can allocate.\n/// Canonically equivalent inputs produce equal NFD text under the same Unicode data version.\npub effect fn normalizeNfd(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let scalars = run decomposedScalars(value)\n return run encodeOwned(move scalars)\n}\n\n/// Returns the Normalization Form C of text: decomposed, canonically ordered, then recomposed.\n///\n/// # When to use\n///\n/// Use this function for ordinary normalized storage and canonical-equivalence comparison. Use\n/// [`normalizeNfd`] when a consumer requires decomposed scalars.\n///\n/// # Details\n///\n/// The function does not change `value`. It returns freshly owned UTF-8 text and can allocate.\n/// Canonically equivalent inputs produce equal NFC text under the same Unicode data version.\npub effect fn normalizeNfc(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let scalars = run decomposedScalars(value)\n let composed = composeInPlace(move scalars)\n return run encodeOwned(move composed)\n}\n', }, { module: 'silk/unicode_tables', @@ -1368,7 +1368,7 @@ export const modules = [ module: 'silk/usize', path: 'silk/usize.silk', sourceIdentity: 'silk/usize', - digest: '23f498881e61006c4fc1c498daa2aef983561a6cda9b751ffcc7500a86b9164e', + digest: 'c402141d6a6f4560775bbc47c060c4e45a753dca2f0d9b94bcc4f6f5f77fcdb9', documentation: 'silk/usize.silk', layer: 'portable', runtimeInventory: [ @@ -1427,18 +1427,18 @@ export const modules = [ ], namespace: 'usize', source: - "//! Pointer-width unsigned integers for lengths, indices, capacities, and allocation sizes.\n//!\n//! # When to use\n//! Use `usize` for values that index target memory or describe its layout. Use a fixed-width\n//! integer for serialized data, protocols, and persistent identifiers whose range must not change\n//! between 32-bit and 64-bit targets.\n//!\n//! # Details\n//! [`BITS`] and [`MAX`] follow the selected target. Ordinary arithmetic, narrowing conversions,\n//! division by zero, and invalid shift counts trap. `checked*` returns [`Option`], `wrapping*`\n//! computes modulo the target width, and `saturating*` clamps at [`MIN`] or [`MAX`].\n//!\n//! [`ZERO`] and [`ONE`] provide typed values where an uncontextualized literal would be `i32`.\n//! Decimal [`parse`] uses the target's range, while [`toText`] allocates owned text.\n//!\n//! # Gotchas\n//! A value valid as usize on a 64-bit target can be out of range on a 32-bit target.\n//!\n//! # Examples\n//! ## Keep a count within the target range\n//! ```silk\n//! import silk.option as Option\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let next = usize.checkedAdd(usize.MAX, usize.ONE)\n//! let recovered = move next\n//! |> Option.unwrapOr(42)\n//! return usize.toI32(recovered)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\n\n/// The largest `usize` value for the compilation target.\n///\n/// # Details\n///\n/// This is 4294967295 on a 32-bit target and 18446744073709551615 on a 64-bit target. Checked\n/// arithmetic rejects results above it.\npub const MAX: usize = Target.usizeMax\n\n/// The smallest `usize` value, which is zero at each pointer width.\npub const MIN: usize = 0\n\n/// The width of `usize` in bits, which is the compilation target's pointer width.\npub const BITS: u32 = Target.pointerBits\n\n/// The `usize` zero value for a count without a typed context.\npub const ZERO: usize = 0\n\n/// The `usize` value one for a step or count that has no other type context.\npub const ONE: usize = 1\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: usize) -> u8 {\n return Intrinsic.usizeToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU8(value)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: usize) -> u16 {\n return Intrinsic.usizeToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU16(value)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: usize) -> u32 {\n return Intrinsic.usizeToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU32(value)\n}\n\n/// Converts `value` exactly to `u64`. Every `usize` value is representable.\npub fn toU64(value: usize) -> u64 {\n return Intrinsic.usizeToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `usize` value is\n/// representable.\npub fn checkedToU64(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU64(value)\n}\n\n/// Returns `value` unchanged as `usize`. Use this function when generic conversion code\n/// can select `usize` as both source and destination.\npub fn toUsize(value: usize) -> usize {\n return Intrinsic.usizeToUsize(value)\n}\n\n/// Returns `Some` with `value` unchanged as `usize`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToUsize(value: usize) -> Option {\n return Intrinsic.usizeCheckedToUsize(value)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: usize) -> i8 {\n return Intrinsic.usizeToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI8(value)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: usize) -> i16 {\n return Intrinsic.usizeToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI16(value)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: usize) -> i32 {\n return Intrinsic.usizeToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI32(value)\n}\n\n/// Converts `value` to `i64`. Traps if `value` is outside the `i64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI64(value: usize) -> i64 {\n return Intrinsic.usizeToI64(value)\n}\n\n/// Converts `value` to `i64`, or returns `None` if `value` is outside the `i64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI64(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI64(value)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: usize) -> isize {\n return Intrinsic.usizeToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: usize) -> Option {\n return Intrinsic.usizeCheckedToIsize(value)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: usize) -> f32 {\n return Intrinsic.usizeToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: usize) -> f64 {\n return Intrinsic.usizeToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `usize` range. Use this function\n/// when overflow is a program error.\npub fn add(left: usize, right: usize) -> usize {\n return Intrinsic.usizeAdd(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `usize` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSubtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `usize` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: usize, right: usize) -> usize {\n return Intrinsic.usizeMultiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: usize, right: usize) -> usize {\n return Intrinsic.usizeDivide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: usize, right: usize) -> usize {\n return Intrinsic.usizeRemainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: usize, right: usize) -> usize {\n return Intrinsic.usizeBitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: usize, right: usize) -> usize {\n return Intrinsic.usizeBitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: usize, right: usize) -> usize {\n return Intrinsic.usizeBitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: usize) -> usize {\n return Intrinsic.usizeBitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: usize, right: usize) -> usize {\n return Intrinsic.usizeShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: usize, right: usize) -> usize {\n return Intrinsic.usizeShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: usize, right: usize) -> usize {\n return Intrinsic.usizeRotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: usize, right: usize) -> usize {\n return Intrinsic.usizeRotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `usize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: usize, right: usize) -> usize {\n return Intrinsic.usizeWrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `usize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: usize, right: usize) -> usize {\n return Intrinsic.usizeWrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `usize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: usize, right: usize) -> usize {\n return Intrinsic.usizeWrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `usize` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedAdd(left, right)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `usize` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedSubtract(left, right)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `usize` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedMultiply(left, right)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedDivide(left, right)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedRemainder(left, right)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: usize, right: usize) -> bool {\n return Intrinsic.usizeEquals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: usize, right: usize) -> bool {\n return Intrinsic.usizeNotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: usize, right: usize) -> bool {\n return Intrinsic.usizeLessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: usize, right: usize) -> bool {\n return Intrinsic.usizeLessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: usize, right: usize) -> bool {\n return Intrinsic.usizeGreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: usize, right: usize) -> bool {\n return Intrinsic.usizeGreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: usize) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `usize`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` outside the target's `usize` range.\npub fn parse(text: string) -> Result {\n return Format.usizeValue(text)\n}\n", + "//! Pointer-width unsigned integers for lengths, indices, capacities, and allocation sizes.\n//!\n//! # When to use\n//! Use `usize` for values that index target memory or describe its layout. Use a fixed-width\n//! integer for serialized data, protocols, and persistent identifiers whose range must not change\n//! between 32-bit and 64-bit targets.\n//!\n//! # Details\n//! [`BITS`] and [`MAX`] follow the selected target. Ordinary arithmetic, narrowing conversions,\n//! division by zero, and invalid shift counts trap. `checked*` returns [`Option`], `wrapping*`\n//! computes modulo the target width, and `saturating*` clamps at [`MIN`] or [`MAX`].\n//!\n//! [`ZERO`] and [`ONE`] provide typed values where an uncontextualized literal would be `i32`.\n//! Decimal [`parse`] uses the target's range, while [`toText`] allocates owned text.\n//!\n//! # Gotchas\n//! A value valid as usize on a 64-bit target can be out of range on a 32-bit target.\n//!\n//! # Examples\n//! ## Keep a count within the target range\n//! ```silk\n//! import silk.option as Option\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let next = usize.checkedAdd(usize.MAX, usize.ONE)\n//! let recovered = move next\n//! |> Option.unwrapOr(42)\n//! return usize.toI32(recovered)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.f32 as f32\nimport silk.f64 as f64\nimport silk.format as Format\nimport silk.format { ParseError }\nimport silk.i16 as i16\nimport silk.i32 as i32\nimport silk.i64 as i64\nimport silk.i8 as i8\nimport silk.isize as isize\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { String }\nimport silk.u16 as u16\nimport silk.u32 as u32\nimport silk.u64 as u64\nimport silk.u8 as u8\n\n/// The largest `usize` value for the compilation target.\n///\n/// # Details\n///\n/// This is 4294967295 on a 32-bit target and 18446744073709551615 on a 64-bit target. Checked\n/// arithmetic rejects results above it.\npub const MAX: usize = Target.usizeMax\n\n/// The smallest `usize` value, which is zero at each pointer width.\npub const MIN: usize = 0\n\n/// The width of `usize` in bits, which is the compilation target's pointer width.\npub const BITS: u32 = Target.pointerBits\n\n/// The `usize` zero value for a count without a typed context.\npub const ZERO: usize = 0\n\n/// The `usize` value one for a step or count that has no other type context.\npub const ONE: usize = 1\n\n/// Converts `value` to `u8`. Traps if `value` is outside the `u8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU8(value: usize) -> u8 {\n return Intrinsic.usizeToU8(value)\n}\n\n/// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU8(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU8>(value, some, none)\n}\n\n/// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU16(value: usize) -> u16 {\n return Intrinsic.usizeToU16(value)\n}\n\n/// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU16(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU16>(value, some, none)\n}\n\n/// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toU32(value: usize) -> u32 {\n return Intrinsic.usizeToU32(value)\n}\n\n/// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToU32(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU32>(value, some, none)\n}\n\n/// Converts `value` exactly to `u64`. Every `usize` value is representable.\npub fn toU64(value: usize) -> u64 {\n return Intrinsic.usizeToU64(value)\n}\n\n/// Converts `value` exactly to `u64` and returns `Some`. Every `usize` value is\n/// representable.\npub fn checkedToU64(value: usize) -> Option {\n return Intrinsic.usizeCheckedToU64>(value, some, none)\n}\n\n/// Returns `value` unchanged as `usize`. Use this function when generic conversion code\n/// can select `usize` as both source and destination.\npub fn toUsize(value: usize) -> usize {\n return Intrinsic.usizeToUsize(value)\n}\n\n/// Returns `Some` with `value` unchanged as `usize`. Use this function when generic\n/// checked-conversion code can select the same source and destination type.\npub fn checkedToUsize(value: usize) -> Option {\n return Intrinsic.usizeCheckedToUsize>(value, some, none)\n}\n\n/// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI8(value: usize) -> i8 {\n return Intrinsic.usizeToI8(value)\n}\n\n/// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI8(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI8>(value, some, none)\n}\n\n/// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI16(value: usize) -> i16 {\n return Intrinsic.usizeToI16(value)\n}\n\n/// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI16(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI16>(value, some, none)\n}\n\n/// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI32(value: usize) -> i32 {\n return Intrinsic.usizeToI32(value)\n}\n\n/// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI32(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI32>(value, some, none)\n}\n\n/// Converts `value` to `i64`. Traps if `value` is outside the `i64` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toI64(value: usize) -> i64 {\n return Intrinsic.usizeToI64(value)\n}\n\n/// Converts `value` to `i64`, or returns `None` if `value` is outside the `i64`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToI64(value: usize) -> Option {\n return Intrinsic.usizeCheckedToI64>(value, some, none)\n}\n\n/// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use\n/// this function when an out-of-range value is a program error.\npub fn toIsize(value: usize) -> isize {\n return Intrinsic.usizeToIsize(value)\n}\n\n/// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize`\n/// range. Use this function when an out-of-range value is input data.\npub fn checkedToIsize(value: usize) -> Option {\n return Intrinsic.usizeCheckedToIsize>(value, some, none)\n}\n\n/// Converts `value` to the nearest `f32` value, with ties to even.\npub fn toF32(value: usize) -> f32 {\n return Intrinsic.usizeToF32(value)\n}\n\n/// Converts `value` to the nearest `f64` value, with ties to even.\npub fn toF64(value: usize) -> f64 {\n return Intrinsic.usizeToF64(value)\n}\n\n/// Returns `left + right` and traps if the result is outside the `usize` range. Use this function\n/// when overflow is a program error.\npub fn add(left: usize, right: usize) -> usize {\n return Intrinsic.usizeAdd(left, right)\n}\n\n/// Returns `left - right` and traps if the result is outside the `usize` range. Use this function\n/// when overflow is a program error.\npub fn subtract(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSubtract(left, right)\n}\n\n/// Returns `left * right` and traps if the result is outside the `usize` range. Use this function\n/// when overflow is a program error.\npub fn multiply(left: usize, right: usize) -> usize {\n return Intrinsic.usizeMultiply(left, right)\n}\n\n/// Returns `left / right`. Traps if `right` is zero. Use this function when a zero\n/// divisor is a program error.\npub fn divide(left: usize, right: usize) -> usize {\n return Intrinsic.usizeDivide(left, right)\n}\n\n/// Returns the remainder of `left / right`. Traps if `right` is zero. Use this function\n/// when a zero divisor is a program error.\npub fn remainder(left: usize, right: usize) -> usize {\n return Intrinsic.usizeRemainder(left, right)\n}\n\n/// Returns the bitwise AND of `left` and `right`.\npub fn bitAnd(left: usize, right: usize) -> usize {\n return Intrinsic.usizeBitAnd(left, right)\n}\n\n/// Returns the bitwise OR of `left` and `right`.\npub fn bitOr(left: usize, right: usize) -> usize {\n return Intrinsic.usizeBitOr(left, right)\n}\n\n/// Returns the bitwise exclusive OR of `left` and `right`.\npub fn bitXor(left: usize, right: usize) -> usize {\n return Intrinsic.usizeBitXor(left, right)\n}\n\n/// Returns `value` with each bit inverted.\npub fn bitNot(value: usize) -> usize {\n return Intrinsic.usizeBitNot(value)\n}\n\n/// Shifts `left` bits left by `right` positions. Traps if `right` is not less than [`BITS`].\npub fn shiftLeft(left: usize, right: usize) -> usize {\n return Intrinsic.usizeShiftLeft(left, right)\n}\n\n/// Shifts `left` bits right by `right` positions and inserts zero bits. Traps if `right` is\n/// not less than [`BITS`].\npub fn shiftRight(left: usize, right: usize) -> usize {\n return Intrinsic.usizeShiftRight(left, right)\n}\n\n/// Rotates the bits of `left` left by `right` positions.\npub fn rotateLeft(left: usize, right: usize) -> usize {\n return Intrinsic.usizeRotateLeft(left, right)\n}\n\n/// Rotates the bits of `left` right by `right` positions.\npub fn rotateRight(left: usize, right: usize) -> usize {\n return Intrinsic.usizeRotateRight(left, right)\n}\n\n/// Returns `left + right`, wrapped to the `usize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingAdd(left: usize, right: usize) -> usize {\n return Intrinsic.usizeWrappingAdd(left, right)\n}\n\n/// Returns `left - right`, wrapped to the `usize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingSubtract(left: usize, right: usize) -> usize {\n return Intrinsic.usizeWrappingSubtract(left, right)\n}\n\n/// Returns `left * right`, wrapped to the `usize` range. Use this function for\n/// deliberate modulo arithmetic.\npub fn wrappingMultiply(left: usize, right: usize) -> usize {\n return Intrinsic.usizeWrappingMultiply(left, right)\n}\n\n/// Returns `left + right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingAdd(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSaturatingAdd(left, right)\n}\n\n/// Returns `left - right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingSubtract(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSaturatingSubtract(left, right)\n}\n\n/// Returns `left * right`, clamped to [`MIN`] or [`MAX`]. Use this function when a\n/// boundary value is the required overflow result.\npub fn saturatingMultiply(left: usize, right: usize) -> usize {\n return Intrinsic.usizeSaturatingMultiply(left, right)\n}\n\n/// Returns `Some` with `left + right`, or `None` if the result is outside the `usize` range.\n/// Use this function when overflow is input data.\npub fn checkedAdd(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedAdd>(left, right, some, none)\n}\n\n/// Returns `Some` with `left - right`, or `None` if the result is outside the `usize` range.\n/// Use this function when overflow is input data.\npub fn checkedSubtract(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedSubtract>(left, right, some, none)\n}\n\n/// Returns `Some` with `left * right`, or `None` if the result is outside the `usize` range.\n/// Use this function when overflow is input data.\npub fn checkedMultiply(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedMultiply>(left, right, some, none)\n}\n\n/// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this\n/// function when a zero divisor is input data.\npub fn checkedDivide(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedDivide>(left, right, some, none)\n}\n\n/// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function\n/// when a zero divisor is input data.\npub fn checkedRemainder(left: usize, right: usize) -> Option {\n return Intrinsic.usizeCheckedRemainder>(left, right, some, none)\n}\n\n/// Returns `true` when `left` and `right` are equal.\npub fn equals(left: usize, right: usize) -> bool {\n return Intrinsic.usizeEquals(left, right)\n}\n\n/// Returns `true` when `left` and `right` are not equal.\npub fn notEquals(left: usize, right: usize) -> bool {\n return Intrinsic.usizeNotEquals(left, right)\n}\n\n/// Returns `true` when `left` is less than `right`.\npub fn lessThan(left: usize, right: usize) -> bool {\n return Intrinsic.usizeLessThan(&left, &right)\n}\n\n/// Returns `true` when `left` is less than or equal to `right`.\npub fn lessOrEqual(left: usize, right: usize) -> bool {\n return Intrinsic.usizeLessOrEqual(left, right)\n}\n\n/// Returns `true` when `left` is greater than `right`.\npub fn greaterThan(left: usize, right: usize) -> bool {\n return Intrinsic.usizeGreaterThan(left, right)\n}\n\n/// Returns `true` when `left` is greater than or equal to `right`.\npub fn greaterOrEqual(left: usize, right: usize) -> bool {\n return Intrinsic.usizeGreaterOrEqual(left, right)\n}\n\n/// Renders the value as base-10 text in new owned storage. Allocation uses the required\n/// `Allocator` and can fail with `OutOfMemoryError`.\npub effect fn toText(value: usize) -> String ! OutOfMemoryError ? &mut Allocator {\n return run Format.unsignedText(toU64(value))\n}\n\n/// Reads the complete text as an unsigned decimal `usize`.\n///\n/// # Details\n///\n/// A failure contains `silk.format.NotANumber` for empty text, a sign, a non-digit, or trailing\n/// bytes. It contains `silk.format.OutOfRange` outside the target's `usize` range.\npub fn parse(text: string) -> Result {\n return Format.usizeValue(text)\n}\n", }, { module: 'silk/vector', path: 'silk/vector.silk', sourceIdentity: 'silk/vector', - digest: '0302e7b347956955708a7089e48df9fdbb6bd7c8c06078dd4090b5d600c33f15', + digest: '8adcbbd293c7c472171ce17cd60ff9099fffdc07ccefb97c5f7f5a39b473ffeb', documentation: 'silk/vector.silk', layer: 'portable', runtimeInventory: ['replace'], namespace: 'Vector', source: - "//! Growable owned sequences with allocation-aware mutation, stable sorting, and checked indexing.\n//!\n//! # When to use\n//! Use [`Vector`] when a sequence must grow or own a runtime-determined number of values. Use a\n//! fixed array when the length is part of the type, and `silk.bytes.Bytes` for bulk byte storage.\n//!\n//! # Details\n//! [`make`] is allocation-free. The first growth reserves four elements and later growth doubles\n//! capacity; [`reserve`] can move that cost ahead of mutation. Growth completes in replacement\n//! storage before committing, so [`append`] and [`reserve`] leave the vector unchanged on\n//! [`OutOfMemoryError`]. Removing, clearing, and truncating drop exactly the elements they discard\n//! while retaining capacity.\n//!\n//! [`sort`] is stable, deterministic, and supports move-only elements, but allocates scratch space.\n//! [`binarySearch`] requires an already sorted vector and returns the lowest index among equal\n//! matches.\n//!\n//! # Gotchas\n//! [`get`], [`set`], and [`remove`] trap on an out-of-range index. An [`insert`] position must be at\n//! or before the current length. Use [`asSlice`] to borrow move-only elements because [`get`]\n//! produces a copied value.\n//!\n//! # Examples\n//! ## Grow and edit an owned sequence\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.vector as Vector\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut values = Vector.make()\n//! let first = run Vector.append(&mut values, 10)\n//! |> Effect.provideMut(&mut allocator)\n//! let second = run Vector.append(&mut values, 30)\n//! |> Effect.provideMut(&mut allocator)\n//! let middle = run Vector.insert(&mut values, 1, 20)\n//! |> Effect.provideMut(&mut allocator)\n//! let changed = Vector.set(&mut values, 2, 22)\n//! let removed = Vector.remove(&mut values, 0)\n//! return Vector.get(&values, 0) + Vector.get(&values, 1)\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n//!\n//! ## Sort values and find the first equal value\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option as Option\n//!\n//! import silk.usize as usize\n//!\n//! import silk.vector as Vector\n//!\n//! effect fn search() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut values = Vector.make()\n//! let first = run Vector.append(&mut values, 3)\n//! |> Effect.provideMut(&mut allocator)\n//! let second = run Vector.append(&mut values, 36)\n//! |> Effect.provideMut(&mut allocator)\n//! let third = run Vector.append(&mut values, 3)\n//! |> Effect.provideMut(&mut allocator)\n//! let sorting = Vector.sort(&mut values)\n//! |> Effect.provideMut(&mut allocator)\n//! let sorted = run sorting\n//! let found = Vector.binarySearch(&values, 3)\n//! |> Option.unwrapOr(99)\n//! if found != usize.ZERO {\n//! return 0\n//! }\n//! return Vector.get(&values, 0) + Vector.get(&values, 1) + Vector.get(&values, 2)\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(search(), recover)\n//! }\n//! ```\n\n// A growable owned sequence over the allocation substrate. Ordinary Silk: no compiler phase\n// knows this type, and every capability it uses is available to user code.\n//\n// The storage union keeps the empty vector allocation-free; Intrinsic.replace moves it out and back\n// through &mut self without a partial move, and the Drop hook destroys exactly the\n// initialized elements before the backing buffer releases.\n\nimport silk.allocator as AllocationFailure\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.layout { Layout }\nimport silk.layout { LayoutOverflow }\nimport silk.option { Option }\nimport silk.order { Order }\nimport silk.raw_buffer as RawBuffer\nimport silk.slot as Slot\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The allocation-free storage state of an empty [`Vector`].\npub struct Empty {\n anchor: [T; 0]\n}\n\n/// The allocated storage state of a non-empty or reserved [`Vector`].\npub struct Full {\n buffer: RawBuffer\n}\n\n/// Owns an initialized prefix of a growable contiguous allocation.\n///\n/// # Details\n///\n/// The vector releases each initialized element and its storage on drop. Length counts initialized\n/// elements. Capacity counts elements that fit without growth.\npub struct Vector {\n storage: Empty | Full\n length: usize\n capacity: usize\n}\n\n// Diverges on the arms the surrounding logic has already proven impossible.\nfn absurd() -> T {\n let boom = 1 / 0\n return absurd()\n}\n\n/// Creates an empty vector with zero capacity and no allocation.\npub fn make() -> Vector {\n return Vector { storage: Empty { anchor: [] }, length: usize.ZERO, capacity: usize.ZERO }\n}\n\n/// Returns the number of initialized elements.\npub fn length(self: &Vector) -> usize {\n return self.length\n}\n\n/// Returns the total number of elements that fit without another growth allocation.\npub fn capacity(self: &Vector) -> usize {\n return self.capacity\n}\n\nfn emptySlice(anchor: &[T]) -> &[T] {\n return anchor\n}\n\nfn emptyMutSlice(anchor: &mut [T]) -> &mut [T] {\n return anchor\n}\n\n/// Borrows the initialized elements as one shared lexical slice.\n///\n/// # Gotchas\n///\n/// Do not retain this slice across an operation that can grow the vector.\npub fn asSlice(self: &Vector) -> &[T] {\n return match &self.storage {\n Empty { anchor } => emptySlice(&anchor)\n Full { buffer } => RawBuffer.view(&buffer, usize.ZERO, self.length)\n }\n}\n\n/// Borrows all initialized elements as one exclusive lexical slice.\n///\n/// # Gotchas\n///\n/// Do not retain this slice across an operation that can grow the vector.\npub fn asMutSlice(self: &mut Vector) -> &mut [T] {\n return match &mut self.storage {\n Empty { anchor } => emptyMutSlice(&mut anchor)\n Full { buffer } => RawBuffer.viewMut(&mut buffer, usize.ZERO, self.length)\n }\n}\n\nimpl Drop for Vector {\n fn drop(self: &mut Vector) -> () {\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let count = Intrinsic.replace(self.length, usize.ZERO)\n return match move storage {\n Empty nothing => ()\n Full full => releaseFull(move full, count)\n }\n }\n}\n\nfn releaseFull(full: Full, length: usize) -> () {\n return match move full {\n Full { buffer } => releaseBuffer(move buffer, length)\n }\n}\n\nfn releaseBuffer(buffer: RawBuffer, length: usize) -> () {\n unsafe {\n let mut owned = move buffer\n let mut index = usize.ZERO\n while index < length {\n let selected = RawBuffer.slot(&mut owned, index)\n let cleared = Slot.dropValue(move selected)\n index = index + usize.ONE\n }\n drop owned\n }\n return ()\n}\n\n// Growth is atomic: the replacement buffer exists and holds every element before the vector's\n// storage commits, so a failed allocation leaves the original untouched.\n/// Appends one owned value, growing geometrically when capacity is exhausted.\n///\n/// # Details\n///\n/// The vector takes ownership of `value`. If growth fails, the vector keeps its prior contents,\n/// length, and capacity.\npub effect fn append(self: &mut Vector, value: T) -> () ! OutOfMemoryError ? &mut Allocator {\n if self.length == self.capacity {\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => writeAt(move full, self.length, move value)\n }\n self.storage = move stored\n self.length = self.length + usize.ONE\n return ()\n}\n\n// Bulk byte append. Concrete to u8 rather than generic over T: the copy moves the source range,\n// and moving out of a borrowed slice is only a copy when the element type is Copy. Growth\n// repeats the shape `append` uses, so a failed allocation leaves the original vector untouched.\n/// Appends every byte of one borrowed sequence in source order with one bulk copy.\n///\n/// # Details\n///\n/// If growth fails, the vector keeps its prior contents, length, and capacity.\npub effect fn appendBytes(\n self: &mut Vector,\n values: &[u8]\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let count = values.length\n if count == usize.ZERO {\n return ()\n }\n let needed = self.length + count\n if self.capacity < needed {\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n while next < needed {\n next = next + next\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => copyInto(move full, self.length, values, count)\n }\n self.storage = move stored\n self.length = needed\n return ()\n}\n\nfn copyInto(full: Full, offset: usize, values: &[u8], count: usize) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: copyBuffer(move buffer, offset, values, count) }\n }\n}\n\nfn copyBuffer(buffer: RawBuffer, offset: usize, values: &[u8], count: usize) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let copied = RawBuffer.copy(&mut owned, offset, values, count)\n return move owned\n }\n return absurd>()\n}\n\n/// Inserts one owned value at an index, shifting later elements without requiring T to be Copy.\n///\n/// # Details\n///\n/// Existing elements from `index` onward move one position to the right. If growth fails, the\n/// vector keeps its prior contents, length, and capacity.\n///\n/// # Gotchas\n///\n/// `index` must be less than or equal to [`length`].\npub effect fn insert(\n self: &mut Vector,\n index: usize,\n value: T\n) -> () ! OutOfMemoryError ? &mut Allocator {\n if self.length == self.capacity {\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => insertInto(move full, index, self.length, move value)\n }\n self.storage = move stored\n self.length = self.length + usize.ONE\n return ()\n}\n\nfn insertInto(full: Full, index: usize, length: usize, value: T) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: insertBuffer(move buffer, index, length, move value) }\n }\n}\n\nfn insertBuffer(buffer: RawBuffer, index: usize, length: usize, value: T) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let mut cursor = length\n while index < cursor {\n let source = RawBuffer.slot(&mut owned, cursor - usize.ONE)\n let shifted = Slot.take(move source)\n let target = RawBuffer.slot(&mut owned, cursor)\n let written = Slot.write(move target, move shifted)\n cursor = cursor - usize.ONE\n }\n let selected = RawBuffer.slot(&mut owned, index)\n let written = Slot.write(move selected, move value)\n return move owned\n }\n return absurd>()\n}\n\neffect fn overflowed() -> Layout ! OutOfMemoryError {\n return run AllocationFailure.outOfMemory()\n}\n\nfn freshBuffer(allocation: Allocation, count: usize) -> RawBuffer {\n unsafe {\n let made = RawBuffer.from(move allocation, count)\n return move made\n }\n return absurd>()\n}\n\nfn migrate(full: Full, allocation: Allocation, length: usize, count: usize) -> RawBuffer {\n return match move full {\n Full { buffer } => migrateBuffer(move buffer, move allocation, length, count)\n }\n}\n\n// One bulk move replaces the element-by-element migration: the initialized prefix of the old\n// buffer travels to the fresh buffer in a single copy, and the emptied source releases as before.\nfn migrateBuffer(old: RawBuffer, allocation: Allocation, length: usize, count: usize) -> RawBuffer {\n let mut target = freshBuffer(move allocation, count)\n unsafe {\n let mut source = move old\n let initialized = RawBuffer.view(&source, usize.ZERO, length)\n let migrated = RawBuffer.copy(&mut target, usize.ZERO, initialized, length)\n drop source\n }\n return move target\n}\n\nfn writeAt(full: Full, index: usize, value: T) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: writeSlot(move buffer, index, move value) }\n }\n}\n\nfn writeSlot(buffer: RawBuffer, index: usize, value: T) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let written = Slot.write(move selected, move value)\n return move owned\n }\n return absurd>()\n}\n\nstruct Read {\n value: T\n}\n\nimpl Copy for Read {}\n\n// Checked read for Copy element types: out-of-range access traps identically on every engine.\n/// Copies the element at one index and traps when the index is out of range.\n///\n/// # When to use\n///\n/// Use this function for a `Copy` element. Use [`asSlice`] to borrow a move-only element.\npub fn get(self: &Vector, index: usize) -> T {\n if self.length <= index {\n let boom = 1 / 0\n }\n let read = match &self.storage {\n Empty nothing => absurd>()\n Full { buffer } => Read { value: readAt(&buffer, index) }\n }\n return match move read {\n Read { value } => move value\n }\n}\n\nfn readAt(buffer: &RawBuffer, index: usize) -> T {\n unsafe {\n return RawBuffer.read(buffer, index)\n }\n return absurd()\n}\n\n// Carries the storage back out alongside the element moved out of it, so a removal never leaves\n// the union behind in `self` while the element travels.\nstruct Taken {\n storage: Full\n value: T\n}\n\nfn takeAt(full: Full, index: usize) -> Taken {\n return match move full {\n Full { buffer } => takeSlot(move buffer, index)\n }\n}\n\nfn takeSlot(buffer: RawBuffer, index: usize) -> Taken {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let value = Slot.take(move selected)\n return Taken { storage: Full { buffer: move owned }, value: move value }\n }\n return absurd>()\n}\n\n/// Removes the last element and returns it. Returns an absent value for an empty vector.\n///\n/// # Details\n///\n/// A present result transfers ownership of the removed element. Capacity does not change.\npub fn pop(self: &mut Vector) -> Option {\n if self.length == usize.ZERO {\n return Option.none()\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let taken = match move storage {\n Empty nothing => absurd>()\n Full full => takeAt(move full, self.length - usize.ONE)\n }\n self.length = self.length - usize.ONE\n return match move taken {\n Taken { storage: kept, value } => finish(move self, move kept, move value)\n }\n}\n\n// Commits the storage back into the vector and yields the removed element as a present optional.\nfn finish(self: &mut Vector, storage: Full, value: T) -> Option {\n self.storage = move storage\n return Option.some(move value)\n}\n\n/// Removes the element at one index, shifting the later elements down. Traps out of range.\n///\n/// # Details\n///\n/// Ownership of the removed element passes to the caller. Capacity does not change.\npub fn remove(self: &mut Vector, index: usize) -> T {\n if self.length <= index {\n let boom = 1 / 0\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let taken = match move storage {\n Empty nothing => absurd>()\n Full full => removeAt(move full, index, self.length)\n }\n self.length = self.length - usize.ONE\n return match move taken {\n Taken { storage: kept, value } => commit(move self, move kept, move value)\n }\n}\n\n// Commits the storage back into the vector and yields the removed element itself.\nfn commit(self: &mut Vector, storage: Full, value: T) -> T {\n self.storage = move storage\n return move value\n}\n\nfn removeAt(full: Full, index: usize, length: usize) -> Taken {\n return match move full {\n Full { buffer } => removeBuffer(move buffer, index, length)\n }\n}\n\n// Rotates the removed element towards the end one swap at a time, then takes it out of the last\n// slot. Keeping the take last means no element is held live across the loop, and every slot holds\n// an initialized value at every step because each iteration swaps a pair rather than clearing one.\n// Takes the element out first, then closes the hole by moving each later element down one slot,\n// mirroring insertBuffer so every slot holds an initialized value at each step.\nfn removeBuffer(buffer: RawBuffer, index: usize, length: usize) -> Taken {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let value = Slot.take(move selected)\n let mut cursor = index + usize.ONE\n while cursor < length {\n let source = RawBuffer.slot(&mut owned, cursor)\n let shifted = Slot.take(move source)\n let target = RawBuffer.slot(&mut owned, cursor - usize.ONE)\n let written = Slot.write(move target, move shifted)\n cursor = cursor + usize.ONE\n }\n return Taken { storage: Full { buffer: move owned }, value: move value }\n }\n return absurd>()\n}\n\n/// Drops every initialized element and sets the length to zero, keeping the capacity.\npub fn clear(self: &mut Vector) -> () {\n return truncate(move self, usize.ZERO)\n}\n\n/// Drops every element past one length, keeping the capacity. Shorter lengths are left alone.\n///\n/// # Details\n///\n/// If `length` is not less than the current length, this function does nothing.\npub fn truncate(self: &mut Vector, length: usize) -> () {\n if self.length <= length {\n return ()\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let kept = match move storage {\n Empty nothing => absurd>()\n Full full => dropRange(move full, length, self.length)\n }\n self.storage = move kept\n self.length = length\n return ()\n}\n\nfn dropRange(full: Full, from: usize, to: usize) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: dropRangeBuffer(move buffer, from, to) }\n }\n}\n\nfn dropRangeBuffer(buffer: RawBuffer, from: usize, to: usize) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let mut index = from\n while index < to {\n let selected = RawBuffer.slot(&mut owned, index)\n let cleared = Slot.dropValue(move selected)\n index = index + usize.ONE\n }\n return move owned\n }\n return absurd>()\n}\n\n/// Overwrites the element at one index, dropping the old element first. Traps out of range.\npub fn set(self: &mut Vector, index: usize, value: T) -> () {\n if self.length <= index {\n let boom = 1 / 0\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => replaceAt(move full, index, move value)\n }\n self.storage = move stored\n return ()\n}\n\nfn replaceAt(full: Full, index: usize, value: T) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: replaceSlot(move buffer, index, move value) }\n }\n}\n\nfn replaceSlot(buffer: RawBuffer, index: usize, value: T) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let cleared = Slot.dropValue(move selected)\n let target = RawBuffer.slot(&mut owned, index)\n let written = Slot.write(move target, move value)\n return move owned\n }\n return absurd>()\n}\n\n// Growth is atomic exactly as in append: the replacement buffer holds every element before the\n// vector's storage commits, so a failed allocation leaves the original untouched.\n\n/// Grows capacity to hold at least `additional` more elements without another allocation.\n///\n/// # Details\n///\n/// This function does not change the length. If allocation fails, contents, length, and capacity\n/// remain unchanged.\npub effect fn reserve(\n self: &mut Vector,\n additional: usize\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let needed = self.length + additional\n if needed <= self.capacity {\n return ()\n }\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n while next < needed {\n next = next + next\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n return ()\n}\n\n// Ordering. A bottom-up merge sort over a permutation of the element indices, so it is stable, and\n// deterministic because every comparison and every exchange is decided by the run boundaries alone\n// and never by an address, a capacity, or an engine detail. The same input therefore produces the\n// same output on the evaluator, on LLVM, and on Wasm.\n//\n// Merging cannot happen in place, so the sort allocates and carries `! OutOfMemoryError ? &mut Allocator`.\n// An insertion sort would need no allocation and no requirement, but it costs O(n^2), and the two\n// signatures are not source-compatible, so the choice cannot be deferred to a later release.\n//\n// The sort orders indices first and moves elements only once, at the end: each element is taken from\n// its old slot into the scratch buffer exactly once and the whole prefix travels back in one bulk\n// move. An element is therefore never read out of a borrowed place, never duplicated, and never\n// dropped twice, and the movement itself never requires the element type to be Copy.\n//\n// `sort` is one single body rather than the helper chain the rest of this file uses because a bound\n// is not carried into a nested generic call: a `T: Order` body cannot pass its own `T` to another\n// `T: Order` function. Every helper `sort` calls is therefore unbounded, and every comparison is\n// written here.\n\nstruct Applied {\n buffer: RawBuffer\n scratch: RawBuffer\n}\n\n/// Orders the elements in place. Equal elements keep their input order.\n///\n/// # Details\n///\n/// The sort is stable and deterministic. It supports move-only elements and allocates scratch\n/// storage. If allocation fails, the vector remains unchanged.\npub effect fn sort(self: &mut Vector) -> () ! OutOfMemoryError ? &mut Allocator {\n let count = self.length\n if count <= usize.ONE {\n return ()\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let scratch = freshBuffer(move allocation, count)\n let mut order = make()\n let mut spare = make()\n let mut seed = usize.ZERO\n while seed < count {\n let placed = run append(&mut order, seed)\n let staged = run append(&mut spare, seed)\n seed = seed + usize.ONE\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let mut owned = takeBuffer(move storage)\n let mut width = usize.ONE\n while width < count {\n let mut low = usize.ZERO\n while low < count {\n let mut middle = low + width\n if count < middle {\n middle = count\n }\n let mut high = middle + width\n if count < high {\n high = count\n }\n let mut left = low\n let mut right = middle\n let mut cursor = low\n while cursor < high {\n // The right run wins a position only when it compares strictly less, so equal elements\n // leave the left run first and the sort is stable.\n let mut takeRight = true\n if left < middle {\n takeRight = false\n if right < high {\n let indices = asSlice(&order)\n let after = RawBuffer.view(&owned, indices[right], usize.ONE)\n let before = RawBuffer.view(&owned, indices[left], usize.ONE)\n takeRight = (&after[usize.ZERO]) < (&before[usize.ZERO])\n }\n }\n let mut picked = usize.ZERO\n if takeRight {\n let indices = asSlice(&order)\n picked = indices[right]\n right = right + usize.ONE\n } else {\n let indices = asSlice(&order)\n picked = indices[left]\n left = left + usize.ONE\n }\n let mut slots = asMutSlice(&mut spare)\n slots[cursor] = picked\n cursor = cursor + usize.ONE\n }\n low = high\n }\n let mut back = usize.ZERO\n while back < count {\n let staged = asSlice(&spare)\n let value = staged[back]\n let mut slots = asMutSlice(&mut order)\n slots[back] = value\n back = back + usize.ONE\n }\n width = width + width\n }\n let applied = applyOrder(move owned, move scratch, asSlice(&order), count)\n let sorted = match move applied {\n Applied { buffer, scratch: leftover } => releaseScratch(move buffer, move leftover)\n }\n self.storage = Full { buffer: move sorted }\n return ()\n}\n\nfn takeBuffer(storage: Empty | Full) -> RawBuffer {\n return match move storage {\n Empty nothing => absurd>()\n Full full => unwrapFull(move full)\n }\n}\n\nfn unwrapFull(full: Full) -> RawBuffer {\n return match move full {\n Full { buffer } => move buffer\n }\n}\n\n// Moves every element into the scratch buffer in the order the permutation names, then returns the\n// whole prefix in one bulk move. Every source slot is taken exactly once because the permutation is\n// a bijection, so nothing leaks and nothing is dropped twice.\nfn applyOrder(buffer: RawBuffer, scratch: RawBuffer, order: &[usize], count: usize) -> Applied {\n let mut owned = move buffer\n let mut staging = move scratch\n unsafe {\n let mut index = usize.ZERO\n while index < count {\n let selected = RawBuffer.slot(&mut owned, order[index])\n let value = Slot.take(move selected)\n let destination = RawBuffer.slot(&mut staging, index)\n let written = Slot.write(move destination, move value)\n index = index + usize.ONE\n }\n }\n let merged = RawBuffer.view(&staging, usize.ZERO, count)\n let moved = RawBuffer.copy(&mut owned, usize.ZERO, merged, count)\n return Applied { buffer: move owned, scratch: move staging }\n}\n\n// The scratch buffer holds no initialized element once the bulk move returns, so releasing it\n// destroys nothing.\nfn releaseScratch(buffer: RawBuffer, leftover: RawBuffer) -> RawBuffer {\n unsafe {\n let spent = move leftover\n drop spent\n }\n return move buffer\n}\n\n/// Returns the index of a matching element in a sorted vector, or an absent value when none matches.\n///\n/// # Details\n///\n/// Returns the lowest matching index when a vector holds several equal elements, so a repeated\n/// search over one vector always answers with the same index.\n/// This function consumes `target` and does not change the vector.\n///\n/// # Gotchas\n///\n/// The vector must already be ordered by the same `Order` witness.\npub fn binarySearch(self: &Vector, target: T) -> Option {\n let two = usize.ONE + usize.ONE\n let values = asSlice(self)\n let mut low = usize.ZERO\n let mut high = self.length\n while low < high {\n let middle = low + (high - low) / two\n if (&values[middle]) < (&target) {\n low = middle + usize.ONE\n } else {\n high = middle\n }\n }\n if low < self.length {\n if !((&target) < (&values[low])) {\n return Option.some(low)\n }\n }\n return Option.none()\n}\n", + "//! Growable owned sequences with allocation-aware mutation, stable sorting, and checked indexing.\n//!\n//! # When to use\n//! Use [`Vector`] when a sequence must grow or own a runtime-determined number of values. Use a\n//! fixed array when the length is part of the type, and `silk.bytes.Bytes` for bulk byte storage.\n//!\n//! # Details\n//! [`make`] is allocation-free. The first growth reserves four elements and later growth doubles\n//! capacity; [`reserve`] can move that cost ahead of mutation. Growth completes in replacement\n//! storage before committing, so [`append`] and [`reserve`] leave the vector unchanged on\n//! [`OutOfMemoryError`]. Removing, clearing, and truncating drop exactly the elements they discard\n//! while retaining capacity.\n//!\n//! [`sort`] is stable, deterministic, and supports move-only elements, but allocates scratch space.\n//! [`binarySearch`] requires an already sorted vector and returns the lowest index among equal\n//! matches.\n//!\n//! # Gotchas\n//! [`get`], [`set`], and [`remove`] trap on an out-of-range index. An [`insert`] position must be at\n//! or before the current length. Use [`asSlice`] to borrow move-only elements because [`get`]\n//! produces a copied value.\n//!\n//! # Examples\n//! ## Grow and edit an owned sequence\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.vector as Vector\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut values = Vector.make()\n//! let first = run Vector.append(&mut values, 10)\n//! |> Effect.provideMut(&mut allocator)\n//! let second = run Vector.append(&mut values, 30)\n//! |> Effect.provideMut(&mut allocator)\n//! let middle = run Vector.insert(&mut values, 1, 20)\n//! |> Effect.provideMut(&mut allocator)\n//! let changed = Vector.set(&mut values, 2, 22)\n//! let removed = Vector.remove(&mut values, 0)\n//! return Vector.get(&values, 0) + Vector.get(&values, 1)\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n//!\n//! ## Sort values and find the first equal value\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option as Option\n//!\n//! import silk.usize as usize\n//!\n//! import silk.vector as Vector\n//!\n//! effect fn search() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let mut values = Vector.make()\n//! let first = run Vector.append(&mut values, 3)\n//! |> Effect.provideMut(&mut allocator)\n//! let second = run Vector.append(&mut values, 36)\n//! |> Effect.provideMut(&mut allocator)\n//! let third = run Vector.append(&mut values, 3)\n//! |> Effect.provideMut(&mut allocator)\n//! let sorting = Vector.sort(&mut values)\n//! |> Effect.provideMut(&mut allocator)\n//! let sorted = run sorting\n//! let found = Vector.binarySearch(&values, 3)\n//! |> Option.unwrapOr(99)\n//! if found != usize.ZERO {\n//! return 0\n//! }\n//! return Vector.get(&values, 0) + Vector.get(&values, 1) + Vector.get(&values, 2)\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(search(), recover)\n//! }\n//! ```\n\n// A growable owned sequence over the allocation substrate. Ordinary Silk: no compiler phase\n// knows this type, and every capability it uses is available to user code.\n//\n// The storage union keeps the empty vector allocation-free; Intrinsic.replace moves it out and back\n// through &mut self without a partial move, and the Drop hook destroys exactly the\n// initialized elements before the backing buffer releases.\n\nimport silk.allocator as AllocationFailure\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.layout { Layout }\nimport silk.layout { LayoutOverflow }\nimport silk.option { Option, none, some }\nimport silk.order { Order }\nimport silk.raw_buffer as RawBuffer\nimport silk.slot as Slot\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// The allocation-free storage state of an empty [`Vector`].\npub struct Empty {\n anchor: [T; 0]\n}\n\n/// The allocated storage state of a non-empty or reserved [`Vector`].\npub struct Full {\n buffer: RawBuffer\n}\n\n/// Owns an initialized prefix of a growable contiguous allocation.\n///\n/// # Details\n///\n/// The vector releases each initialized element and its storage on drop. Length counts initialized\n/// elements. Capacity counts elements that fit without growth.\npub struct Vector {\n storage: Empty | Full\n length: usize\n capacity: usize\n}\n\n// Diverges on the arms the surrounding logic has already proven impossible.\nfn absurd() -> T {\n let boom = 1 / 0\n return absurd()\n}\n\n/// Creates an empty vector with zero capacity and no allocation.\npub fn make() -> Vector {\n return Vector { storage: Empty { anchor: [] }, length: usize.ZERO, capacity: usize.ZERO }\n}\n\n/// Returns the number of initialized elements.\npub fn length(self: &Vector) -> usize {\n return self.length\n}\n\n/// Returns the total number of elements that fit without another growth allocation.\npub fn capacity(self: &Vector) -> usize {\n return self.capacity\n}\n\nfn emptySlice(anchor: &[T]) -> &[T] {\n return anchor\n}\n\nfn emptyMutSlice(anchor: &mut [T]) -> &mut [T] {\n return anchor\n}\n\n/// Borrows the initialized elements as one shared lexical slice.\n///\n/// # Gotchas\n///\n/// Do not retain this slice across an operation that can grow the vector.\npub fn asSlice(self: &Vector) -> &[T] {\n return match &self.storage {\n Empty { anchor } => emptySlice(&anchor)\n Full { buffer } => RawBuffer.view(&buffer, usize.ZERO, self.length)\n }\n}\n\n/// Borrows all initialized elements as one exclusive lexical slice.\n///\n/// # Gotchas\n///\n/// Do not retain this slice across an operation that can grow the vector.\npub fn asMutSlice(self: &mut Vector) -> &mut [T] {\n return match &mut self.storage {\n Empty { anchor } => emptyMutSlice(&mut anchor)\n Full { buffer } => RawBuffer.viewMut(&mut buffer, usize.ZERO, self.length)\n }\n}\n\nimpl Drop for Vector {\n fn drop(self: &mut Vector) -> () {\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let count = Intrinsic.replace(self.length, usize.ZERO)\n return match move storage {\n Empty nothing => ()\n Full full => releaseFull(move full, count)\n }\n }\n}\n\nfn releaseFull(full: Full, length: usize) -> () {\n return match move full {\n Full { buffer } => releaseBuffer(move buffer, length)\n }\n}\n\nfn releaseBuffer(buffer: RawBuffer, length: usize) -> () {\n unsafe {\n let mut owned = move buffer\n let mut index = usize.ZERO\n while index < length {\n let selected = RawBuffer.slot(&mut owned, index)\n let cleared = Slot.dropValue(move selected)\n index = index + usize.ONE\n }\n drop owned\n }\n return ()\n}\n\n// Growth is atomic: the replacement buffer exists and holds every element before the vector's\n// storage commits, so a failed allocation leaves the original untouched.\n/// Appends one owned value, growing geometrically when capacity is exhausted.\n///\n/// # Details\n///\n/// The vector takes ownership of `value`. If growth fails, the vector keeps its prior contents,\n/// length, and capacity.\npub effect fn append(self: &mut Vector, value: T) -> () ! OutOfMemoryError ? &mut Allocator {\n if self.length == self.capacity {\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => writeAt(move full, self.length, move value)\n }\n self.storage = move stored\n self.length = self.length + usize.ONE\n return ()\n}\n\n// Bulk byte append. Concrete to u8 rather than generic over T: the copy moves the source range,\n// and moving out of a borrowed slice is only a copy when the element type is Copy. Growth\n// repeats the shape `append` uses, so a failed allocation leaves the original vector untouched.\n/// Appends every byte of one borrowed sequence in source order with one bulk copy.\n///\n/// # Details\n///\n/// If growth fails, the vector keeps its prior contents, length, and capacity.\npub effect fn appendBytes(\n self: &mut Vector,\n values: &[u8]\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let count = values.length\n if count == usize.ZERO {\n return ()\n }\n let needed = self.length + count\n if self.capacity < needed {\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n while next < needed {\n next = next + next\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => copyInto(move full, self.length, values, count)\n }\n self.storage = move stored\n self.length = needed\n return ()\n}\n\nfn copyInto(full: Full, offset: usize, values: &[u8], count: usize) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: copyBuffer(move buffer, offset, values, count) }\n }\n}\n\nfn copyBuffer(buffer: RawBuffer, offset: usize, values: &[u8], count: usize) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let copied = RawBuffer.copy(&mut owned, offset, values, count)\n return move owned\n }\n return absurd>()\n}\n\n/// Inserts one owned value at an index, shifting later elements without requiring T to be Copy.\n///\n/// # Details\n///\n/// Existing elements from `index` onward move one position to the right. If growth fails, the\n/// vector keeps its prior contents, length, and capacity.\n///\n/// # Gotchas\n///\n/// `index` must be less than or equal to [`length`].\npub effect fn insert(\n self: &mut Vector,\n index: usize,\n value: T\n) -> () ! OutOfMemoryError ? &mut Allocator {\n if self.length == self.capacity {\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => insertInto(move full, index, self.length, move value)\n }\n self.storage = move stored\n self.length = self.length + usize.ONE\n return ()\n}\n\nfn insertInto(full: Full, index: usize, length: usize, value: T) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: insertBuffer(move buffer, index, length, move value) }\n }\n}\n\nfn insertBuffer(buffer: RawBuffer, index: usize, length: usize, value: T) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let mut cursor = length\n while index < cursor {\n let source = RawBuffer.slot(&mut owned, cursor - usize.ONE)\n let shifted = Slot.take(move source)\n let target = RawBuffer.slot(&mut owned, cursor)\n let written = Slot.write(move target, move shifted)\n cursor = cursor - usize.ONE\n }\n let selected = RawBuffer.slot(&mut owned, index)\n let written = Slot.write(move selected, move value)\n return move owned\n }\n return absurd>()\n}\n\neffect fn overflowed() -> Layout ! OutOfMemoryError {\n return run AllocationFailure.outOfMemory()\n}\n\nfn freshBuffer(allocation: Allocation, count: usize) -> RawBuffer {\n unsafe {\n let made = RawBuffer.from(move allocation, count)\n return move made\n }\n return absurd>()\n}\n\nfn migrate(full: Full, allocation: Allocation, length: usize, count: usize) -> RawBuffer {\n return match move full {\n Full { buffer } => migrateBuffer(move buffer, move allocation, length, count)\n }\n}\n\n// One bulk move replaces the element-by-element migration: the initialized prefix of the old\n// buffer travels to the fresh buffer in a single copy, and the emptied source releases as before.\nfn migrateBuffer(old: RawBuffer, allocation: Allocation, length: usize, count: usize) -> RawBuffer {\n let mut target = freshBuffer(move allocation, count)\n unsafe {\n let mut source = move old\n let initialized = RawBuffer.view(&source, usize.ZERO, length)\n let migrated = RawBuffer.copy(&mut target, usize.ZERO, initialized, length)\n drop source\n }\n return move target\n}\n\nfn writeAt(full: Full, index: usize, value: T) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: writeSlot(move buffer, index, move value) }\n }\n}\n\nfn writeSlot(buffer: RawBuffer, index: usize, value: T) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let written = Slot.write(move selected, move value)\n return move owned\n }\n return absurd>()\n}\n\nstruct Read {\n value: T\n}\n\nimpl Copy for Read {}\n\n// Checked read for Copy element types: out-of-range access traps identically on every engine.\n/// Copies the element at one index and traps when the index is out of range.\n///\n/// # When to use\n///\n/// Use this function for a `Copy` element. Use [`asSlice`] to borrow a move-only element.\npub fn get(self: &Vector, index: usize) -> T {\n if self.length <= index {\n let boom = 1 / 0\n }\n let read = match &self.storage {\n Empty nothing => absurd>()\n Full { buffer } => Read { value: readAt(&buffer, index) }\n }\n return match move read {\n Read { value } => move value\n }\n}\n\nfn readAt(buffer: &RawBuffer, index: usize) -> T {\n unsafe {\n return RawBuffer.read(buffer, index)\n }\n return absurd()\n}\n\n// Carries the storage back out alongside the element moved out of it, so a removal never leaves\n// the union behind in `self` while the element travels.\nstruct Taken {\n storage: Full\n value: T\n}\n\nfn takeAt(full: Full, index: usize) -> Taken {\n return match move full {\n Full { buffer } => takeSlot(move buffer, index)\n }\n}\n\nfn takeSlot(buffer: RawBuffer, index: usize) -> Taken {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let value = Slot.take(move selected)\n return Taken { storage: Full { buffer: move owned }, value: move value }\n }\n return absurd>()\n}\n\n/// Removes the last element and returns it. Returns an absent value for an empty vector.\n///\n/// # Details\n///\n/// A present result transfers ownership of the removed element. Capacity does not change.\npub fn pop(self: &mut Vector) -> Option {\n if self.length == usize.ZERO {\n return none()\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let taken = match move storage {\n Empty nothing => absurd>()\n Full full => takeAt(move full, self.length - usize.ONE)\n }\n self.length = self.length - usize.ONE\n return match move taken {\n Taken { storage: kept, value } => finish(move self, move kept, move value)\n }\n}\n\n// Commits the storage back into the vector and yields the removed element as a present optional.\nfn finish(self: &mut Vector, storage: Full, value: T) -> Option {\n self.storage = move storage\n return some(move value)\n}\n\n/// Removes the element at one index, shifting the later elements down. Traps out of range.\n///\n/// # Details\n///\n/// Ownership of the removed element passes to the caller. Capacity does not change.\npub fn remove(self: &mut Vector, index: usize) -> T {\n if self.length <= index {\n let boom = 1 / 0\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let taken = match move storage {\n Empty nothing => absurd>()\n Full full => removeAt(move full, index, self.length)\n }\n self.length = self.length - usize.ONE\n return match move taken {\n Taken { storage: kept, value } => commit(move self, move kept, move value)\n }\n}\n\n// Commits the storage back into the vector and yields the removed element itself.\nfn commit(self: &mut Vector, storage: Full, value: T) -> T {\n self.storage = move storage\n return move value\n}\n\nfn removeAt(full: Full, index: usize, length: usize) -> Taken {\n return match move full {\n Full { buffer } => removeBuffer(move buffer, index, length)\n }\n}\n\n// Rotates the removed element towards the end one swap at a time, then takes it out of the last\n// slot. Keeping the take last means no element is held live across the loop, and every slot holds\n// an initialized value at every step because each iteration swaps a pair rather than clearing one.\n// Takes the element out first, then closes the hole by moving each later element down one slot,\n// mirroring insertBuffer so every slot holds an initialized value at each step.\nfn removeBuffer(buffer: RawBuffer, index: usize, length: usize) -> Taken {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let value = Slot.take(move selected)\n let mut cursor = index + usize.ONE\n while cursor < length {\n let source = RawBuffer.slot(&mut owned, cursor)\n let shifted = Slot.take(move source)\n let target = RawBuffer.slot(&mut owned, cursor - usize.ONE)\n let written = Slot.write(move target, move shifted)\n cursor = cursor + usize.ONE\n }\n return Taken { storage: Full { buffer: move owned }, value: move value }\n }\n return absurd>()\n}\n\n/// Drops every initialized element and sets the length to zero, keeping the capacity.\npub fn clear(self: &mut Vector) -> () {\n return truncate(move self, usize.ZERO)\n}\n\n/// Drops every element past one length, keeping the capacity. Shorter lengths are left alone.\n///\n/// # Details\n///\n/// If `length` is not less than the current length, this function does nothing.\npub fn truncate(self: &mut Vector, length: usize) -> () {\n if self.length <= length {\n return ()\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let kept = match move storage {\n Empty nothing => absurd>()\n Full full => dropRange(move full, length, self.length)\n }\n self.storage = move kept\n self.length = length\n return ()\n}\n\nfn dropRange(full: Full, from: usize, to: usize) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: dropRangeBuffer(move buffer, from, to) }\n }\n}\n\nfn dropRangeBuffer(buffer: RawBuffer, from: usize, to: usize) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let mut index = from\n while index < to {\n let selected = RawBuffer.slot(&mut owned, index)\n let cleared = Slot.dropValue(move selected)\n index = index + usize.ONE\n }\n return move owned\n }\n return absurd>()\n}\n\n/// Overwrites the element at one index, dropping the old element first. Traps out of range.\npub fn set(self: &mut Vector, index: usize, value: T) -> () {\n if self.length <= index {\n let boom = 1 / 0\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let stored = match move storage {\n Empty nothing => absurd>()\n Full full => replaceAt(move full, index, move value)\n }\n self.storage = move stored\n return ()\n}\n\nfn replaceAt(full: Full, index: usize, value: T) -> Full {\n return match move full {\n Full { buffer } => Full { buffer: replaceSlot(move buffer, index, move value) }\n }\n}\n\nfn replaceSlot(buffer: RawBuffer, index: usize, value: T) -> RawBuffer {\n unsafe {\n let mut owned = move buffer\n let selected = RawBuffer.slot(&mut owned, index)\n let cleared = Slot.dropValue(move selected)\n let target = RawBuffer.slot(&mut owned, index)\n let written = Slot.write(move target, move value)\n return move owned\n }\n return absurd>()\n}\n\n// Growth is atomic exactly as in append: the replacement buffer holds every element before the\n// vector's storage commits, so a failed allocation leaves the original untouched.\n\n/// Grows capacity to hold at least `additional` more elements without another allocation.\n///\n/// # Details\n///\n/// This function does not change the length. If allocation fails, contents, length, and capacity\n/// remain unchanged.\npub effect fn reserve(\n self: &mut Vector,\n additional: usize\n) -> () ! OutOfMemoryError ? &mut Allocator {\n let needed = self.length + additional\n if needed <= self.capacity {\n return ()\n }\n let mut next = self.capacity + self.capacity\n if self.capacity == usize.ZERO {\n next = 4\n }\n while next < needed {\n next = next + next\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, next)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let moved = match move storage {\n Empty nothing => Full { buffer: freshBuffer(move allocation, next) }\n Full full => Full { buffer: migrate(move full, move allocation, self.length, next) }\n }\n self.storage = move moved\n self.capacity = next\n return ()\n}\n\n// Ordering. A bottom-up merge sort over a permutation of the element indices, so it is stable, and\n// deterministic because every comparison and every exchange is decided by the run boundaries alone\n// and never by an address, a capacity, or an engine detail. The same input therefore produces the\n// same output on the evaluator, on LLVM, and on Wasm.\n//\n// Merging cannot happen in place, so the sort allocates and carries `! OutOfMemoryError ? &mut Allocator`.\n// An insertion sort would need no allocation and no requirement, but it costs O(n^2), and the two\n// signatures are not source-compatible, so the choice cannot be deferred to a later release.\n//\n// The sort orders indices first and moves elements only once, at the end: each element is taken from\n// its old slot into the scratch buffer exactly once and the whole prefix travels back in one bulk\n// move. An element is therefore never read out of a borrowed place, never duplicated, and never\n// dropped twice, and the movement itself never requires the element type to be Copy.\n//\n// `sort` is one single body rather than the helper chain the rest of this file uses because a bound\n// is not carried into a nested generic call: a `T: Order` body cannot pass its own `T` to another\n// `T: Order` function. Every helper `sort` calls is therefore unbounded, and every comparison is\n// written here.\n\nstruct Applied {\n buffer: RawBuffer\n scratch: RawBuffer\n}\n\n/// Orders the elements in place. Equal elements keep their input order.\n///\n/// # Details\n///\n/// The sort is stable and deterministic. It supports move-only elements and allocates scratch\n/// storage. If allocation fails, the vector remains unchanged.\npub effect fn sort(self: &mut Vector) -> () ! OutOfMemoryError ? &mut Allocator {\n let count = self.length\n if count <= usize.ONE {\n return ()\n }\n let element = Layout.of()\n let plan = Layout.repeat(move element, count)\n let layout = match move plan {\n Layout ready => ready\n LayoutOverflow overflow => run overflowed()\n }\n let recipe = Allocator.allocate(move layout)\n let allocation = run recipe\n let scratch = freshBuffer(move allocation, count)\n let mut order = make()\n let mut spare = make()\n let mut seed = usize.ZERO\n while seed < count {\n let placed = run append(&mut order, seed)\n let staged = run append(&mut spare, seed)\n seed = seed + usize.ONE\n }\n let storage = Intrinsic.replace(self.storage, Empty { anchor: [] })\n let mut owned = takeBuffer(move storage)\n let mut width = usize.ONE\n while width < count {\n let mut low = usize.ZERO\n while low < count {\n let mut middle = low + width\n if count < middle {\n middle = count\n }\n let mut high = middle + width\n if count < high {\n high = count\n }\n let mut left = low\n let mut right = middle\n let mut cursor = low\n while cursor < high {\n // The right run wins a position only when it compares strictly less, so equal elements\n // leave the left run first and the sort is stable.\n let mut takeRight = true\n if left < middle {\n takeRight = false\n if right < high {\n let indices = asSlice(&order)\n let after = RawBuffer.view(&owned, indices[right], usize.ONE)\n let before = RawBuffer.view(&owned, indices[left], usize.ONE)\n takeRight = (&after[usize.ZERO]) < (&before[usize.ZERO])\n }\n }\n let mut picked = usize.ZERO\n if takeRight {\n let indices = asSlice(&order)\n picked = indices[right]\n right = right + usize.ONE\n } else {\n let indices = asSlice(&order)\n picked = indices[left]\n left = left + usize.ONE\n }\n let mut slots = asMutSlice(&mut spare)\n slots[cursor] = picked\n cursor = cursor + usize.ONE\n }\n low = high\n }\n let mut back = usize.ZERO\n while back < count {\n let staged = asSlice(&spare)\n let value = staged[back]\n let mut slots = asMutSlice(&mut order)\n slots[back] = value\n back = back + usize.ONE\n }\n width = width + width\n }\n let applied = applyOrder(move owned, move scratch, asSlice(&order), count)\n let sorted = match move applied {\n Applied { buffer, scratch: leftover } => releaseScratch(move buffer, move leftover)\n }\n self.storage = Full { buffer: move sorted }\n return ()\n}\n\nfn takeBuffer(storage: Empty | Full) -> RawBuffer {\n return match move storage {\n Empty nothing => absurd>()\n Full full => unwrapFull(move full)\n }\n}\n\nfn unwrapFull(full: Full) -> RawBuffer {\n return match move full {\n Full { buffer } => move buffer\n }\n}\n\n// Moves every element into the scratch buffer in the order the permutation names, then returns the\n// whole prefix in one bulk move. Every source slot is taken exactly once because the permutation is\n// a bijection, so nothing leaks and nothing is dropped twice.\nfn applyOrder(buffer: RawBuffer, scratch: RawBuffer, order: &[usize], count: usize) -> Applied {\n let mut owned = move buffer\n let mut staging = move scratch\n unsafe {\n let mut index = usize.ZERO\n while index < count {\n let selected = RawBuffer.slot(&mut owned, order[index])\n let value = Slot.take(move selected)\n let destination = RawBuffer.slot(&mut staging, index)\n let written = Slot.write(move destination, move value)\n index = index + usize.ONE\n }\n }\n let merged = RawBuffer.view(&staging, usize.ZERO, count)\n let moved = RawBuffer.copy(&mut owned, usize.ZERO, merged, count)\n return Applied { buffer: move owned, scratch: move staging }\n}\n\n// The scratch buffer holds no initialized element once the bulk move returns, so releasing it\n// destroys nothing.\nfn releaseScratch(buffer: RawBuffer, leftover: RawBuffer) -> RawBuffer {\n unsafe {\n let spent = move leftover\n drop spent\n }\n return move buffer\n}\n\n/// Returns the index of a matching element in a sorted vector, or an absent value when none matches.\n///\n/// # Details\n///\n/// Returns the lowest matching index when a vector holds several equal elements, so a repeated\n/// search over one vector always answers with the same index.\n/// This function consumes `target` and does not change the vector.\n///\n/// # Gotchas\n///\n/// The vector must already be ordered by the same `Order` witness.\npub fn binarySearch(self: &Vector, target: T) -> Option {\n let two = usize.ONE + usize.ONE\n let values = asSlice(self)\n let mut low = usize.ZERO\n let mut high = self.length\n while low < high {\n let middle = low + (high - low) / two\n if (&values[middle]) < (&target) {\n low = middle + usize.ONE\n } else {\n high = middle\n }\n }\n if low < self.length {\n if !((&target) < (&values[low])) {\n return some(low)\n }\n }\n return none()\n}\n", }, ] as const diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 32025a2f9..b24438007 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '76dde9bb4b040c3b6836570b9ddb381b8d934c61d02d89ef92349e660c0fe6e2' +export const compilerDigest = '9c5a1cb10a043a80930f43ee22f855c6562359b21e1d989e53a3df10154dc907' diff --git a/packages/compiler/src/ValueType.ts b/packages/compiler/src/ValueType.ts index 3a17ffcca..2627acfcc 100644 --- a/packages/compiler/src/ValueType.ts +++ b/packages/compiler/src/ValueType.ts @@ -560,6 +560,15 @@ export const functionItemValueType = ( ): Extract | undefined => { const type = Type.substitute(Type.substitute(item.type, fn.substitution), applicationSubstitution) return Type.isCallable(type) && Type.isRuntimeConcrete(type) - ? Object.freeze({ _tag: 'CallableValue', type, target: item.target }) + ? Object.freeze({ + _tag: 'CallableValue', + type, + target: item.target, + typeArguments: Object.freeze( + item.typeArguments.map((argument) => + Type.substituteGenericArgument(argument, fn.substitution), + ), + ), + }) : undefined } diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 2aa4df508..368107f21 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -6923,33 +6923,22 @@ const emitCheckedScalarOperation = ( operation: Extract, state: WasmOperationContext, ): ReadonlyArray => { - const { layout, plan, slots, scalar } = state + const { layout, plan, scalar } = state - const destination = slots(operation.destination) - const tag = destination.at(0) - const payload = destination.at(1) + const valid = scalar(operation.valid) + const value = scalar(operation.value) const left = operation.operands.at(0) const right = operation.operands.at(1) const source = Scalar.find(operation.sourceType._tag) const target = Scalar.find(operation.valueType._tag) if ( operation.operation === 'CheckedConvertToChar' && - tag !== undefined && - payload !== undefined && left !== undefined && source?.spelling === 'u32' && target?.category === 'Character' ) { - const successOrdinal = operation.type.type.members.findIndex((member) => - SilkType.equals(member, operation.success), - ) - const failureOrdinal = operation.type.type.members.findIndex((member) => - SilkType.equals(member, operation.failure), - ) - if (successOrdinal < 0 || failureOrdinal < 0) - throw new RangeError('Wasm checked char operation lost its Option members') const leftSlot = scalar(left) - return [ + const raw = [ Instr.localGet(leftSlot), Instr.i32Const(0x10ffff), Instr.op('i32.gt_u'), @@ -6961,24 +6950,15 @@ const emitCheckedScalarOperation = ( Instr.op('i32.le_u'), Instr.op('i32.and'), Instr.op('i32.or'), - Instr.localSet(tag), - Instr.i32Const(failureOrdinal), - Instr.i32Const(successOrdinal), - Instr.localGet(tag), - Instr.op('select'), - Instr.localSet(tag), + Instr.op('i32.eqz'), + Instr.localSet(valid), Instr.localGet(leftSlot), - Instr.localSet(payload), + Instr.localSet(value), ] + return [...raw, ...emitCheckedScalarCarrier(operation, state)] } - if ( - tag === undefined || - payload === undefined || - left === undefined || - source?.category !== 'Integer' || - target?.category !== 'Integer' - ) - throw new RangeError('Wasm checked scalar operation lost its Option lanes') + if (left === undefined || source?.category !== 'Integer' || target?.category !== 'Integer') + throw new RangeError('Wasm checked scalar operation lost its scalar lanes') const leftSlot = scalar(left) const rightSlot = right === undefined ? undefined : scalar(right) const pointerBits = plan.target.pointerSize === 4 ? 32 : 64 @@ -6990,21 +6970,7 @@ const emitCheckedScalarOperation = ( sourceBits === 64 ? Instr.i64Const(value) : Instr.i32Const(Number(value)) const targetConstant = (value: bigint): Instr.Instr => targetBits === 64 ? Instr.i64Const(value) : Instr.i32Const(Number(value)) - const successOrdinal = operation.type.type.members.findIndex((member) => - SilkType.equals(member, operation.success), - ) - const failureOrdinal = operation.type.type.members.findIndex((member) => - SilkType.equals(member, operation.failure), - ) - if (successOrdinal < 0 || failureOrdinal < 0) - throw new RangeError('Wasm checked scalar operation lost its Option members') - const setTag = [ - Instr.i32Const(failureOrdinal), - Instr.i32Const(successOrdinal), - Instr.localGet(tag), - Instr.op('select'), - Instr.localSet(tag), - ] + const setValid = [Instr.localGet(valid), Instr.op('i32.eqz'), Instr.localSet(valid)] if (operation.operation.startsWith('CheckedConvertTo')) { const sourceRange = Scalar.range(source, pointerBits) const targetRange = Scalar.range(target, pointerBits) @@ -7030,15 +6996,16 @@ const emitCheckedScalarOperation = ( } else if (sourceBits === 64 && targetBits < 64) { conversion = [Instr.op('i32.wrap_i64')] } - return [ + const raw = [ ...(invalid.length === 0 ? [Instr.i32Const(0)] : invalid), - Instr.localSet(tag), - ...setTag, + Instr.localSet(valid), + ...setValid, Instr.localGet(leftSlot), ...conversion, ...normalizeSubword(targetBits, target.signedness === 'Signed'), - Instr.localSet(payload), + Instr.localSet(value), ] + return [...raw, ...emitCheckedScalarCarrier(operation, state)] } if (rightSlot === undefined) throw new RangeError('Wasm checked arithmetic lost its right operand') @@ -7062,14 +7029,15 @@ const emitCheckedScalarOperation = ( throw new RangeError('Wasm checked arithmetic lost its operation') } const resultScratch = targetBits === 64 ? layout.scratch64 : layout.scratch - return [ + const raw = [ ...checkedArithmeticOutcome(shape, target, leftSlot, rightSlot, resultScratch, pointerBits), - Instr.localSet(tag), - ...setTag, + Instr.localSet(valid), + ...setValid, Instr.localGet(resultScratch), ...normalizeSubword(targetBits, target.signedness === 'Signed'), - Instr.localSet(payload), + Instr.localSet(value), ] + return [...raw, ...emitCheckedScalarCarrier(operation, state)] } const minimum = Scalar.range(target, pointerBits).minimum const signedOverflow = @@ -7090,21 +7058,80 @@ const emitCheckedScalarOperation = ( operation.operation === 'CheckedDivide' ? `${targetPrefix}.div_${signedness}` : `${targetPrefix}.rem_${signedness}` - return [ + const raw = [ Instr.localGet(rightSlot), Instr.op(`${targetPrefix}.eqz`), ...signedOverflow, Instr.op('i32.or'), - Instr.localSet(tag), - Instr.localGet(tag), + Instr.localSet(valid), + Instr.localGet(valid), Instr.ifElse( Instr.valueBlockType(targetBits === 64 ? i64 : i32), [targetConstant(0n)], [Instr.localGet(leftSlot), Instr.localGet(rightSlot), Instr.op(division)], ), ...normalizeSubword(targetBits, target.signedness === 'Signed'), - Instr.localSet(payload), - ...setTag, + Instr.localSet(value), + ...setValid, + ] + return [...raw, ...emitCheckedScalarCarrier(operation, state)] +} + +const emitCheckedScalarCarrier = ( + operation: Extract, + state: WasmOperationContext, +): ReadonlyArray => { + const presentType = state.layout.types.at(operation.present.ordinal) + const absentType = state.layout.types.at(operation.absent.ordinal) + if (presentType?._tag !== 'CallableValue' || absentType?._tag !== 'CallableValue') + throw new RangeError('Wasm checked scalar operation lost its carrier callables') + const apply = ( + callable: Mir.LocalId, + callableType: Extract, + arguments_: ReadonlyArray, + ): ReadonlyArray => + emitApplyCallableOperation( + Object.freeze({ + _tag: 'ApplyCallable', + destination: operation.destination, + callable, + typeArguments: + callableType.environment?.callable.typeArguments ?? + callableType.storage?.realization.targetArguments ?? + callableType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: arguments_, + callableType: callableType.type, + access: callableType.type.mode, + evaluation: 'CalleeThenArguments', + realization: 'Environment', + type: operation.type, + provenance: operation.provenance, + }), + state, + ) + const drop = ( + local: Mir.LocalId, + cleanup: Extract['cleanup'], + ): ReadonlyArray => + emitDropOperation( + Object.freeze({ _tag: 'Drop', local, cleanup, provenance: operation.provenance }), + state, + ) + return [ + Instr.localGet(state.scalar(operation.valid)), + Instr.ifElse( + Instr.emptyBlockType, + [ + ...drop(operation.absent, operation.absentCleanup), + ...apply(operation.present, presentType, Object.freeze([operation.value])), + ], + [ + ...drop(operation.present, operation.presentCleanup), + ...apply(operation.absent, absentType, Object.freeze([])), + ], + ), ] } diff --git a/packages/compiler/stdlib/silk/char.silk b/packages/compiler/stdlib/silk/char.silk index cebab6d85..d04913ced 100644 --- a/packages/compiler/stdlib/silk/char.silk +++ b/packages/compiler/stdlib/silk/char.silk @@ -38,13 +38,13 @@ //! ``` import silk.bool as bool -import silk.option { Option } +import silk.option { Option, none, some } import silk.u32 as u32 /// Converts an integer to a Unicode scalar. Returns `None` for `0xD800` through `0xDFFF` and values /// above `0x10FFFF`. pub fn fromU32(value: u32) -> Option { - return Intrinsic.charFromU32(value) + return Intrinsic.charFromU32>(value, some, none) } /// Returns the exact integer value of an already valid Unicode scalar. diff --git a/packages/compiler/stdlib/silk/child_process.silk b/packages/compiler/stdlib/silk/child_process.silk index f470c7cb6..61ff683ef 100644 --- a/packages/compiler/stdlib/silk/child_process.silk +++ b/packages/compiler/stdlib/silk/child_process.silk @@ -59,8 +59,8 @@ //! |> Effect.provideMut(&mut provider) //! |> Effect.provideMut(&mut allocator) //! return match move Process.exitCode(&outcome) { -//! Option.Some {value} => 35 + value -//! Option.None {} => 1 +//! Option.Option.Some {value} => 35 + value +//! Option.Option.None => 1 //! } //! } //! @@ -91,7 +91,7 @@ import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } import silk.filesystem { Path, rawBytes as pathRawBytes } import silk.i32 as i32 -import silk.option { None, Option, Some, none, some } +import silk.option { Option, none, some } import silk.u8 as u8 import silk.usize as usize diff --git a/packages/compiler/stdlib/silk/effect.silk b/packages/compiler/stdlib/silk/effect.silk index 8a87b4d55..c4accc7b9 100644 --- a/packages/compiler/stdlib/silk/effect.silk +++ b/packages/compiler/stdlib/silk/effect.silk @@ -121,7 +121,7 @@ import silk.bool as bool import silk.logger { LogError, LogLevel, Logger } -import silk.result { Result, Success, Failure } +import silk.result { Result } import silk.usize as usize /// The importable name of the `silk.effect` module scope. @@ -221,10 +221,8 @@ pub effect fn logError( /// pub fn main() -> i32 { /// let completed = run Effect.result(load()) /// return match move completed { -/// Result.Result {value: outcome} => match move outcome { -/// Result.Success {value} => value -/// Result.Failure {error} => error.answer -/// } +/// Result.Result.Success {value} => value +/// Result.Result.Failure {error} => error.answer /// } /// } /// ``` @@ -251,10 +249,8 @@ pub effect fn mapBoth( ) -> B ! F ? R { let completed = run result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => onSuccess(move success) - Failure { error } => run raise(onFailure(move error)) - } + Result.Success { value: success } => onSuccess(move success) + Result.Failure { error } => run raise(onFailure(move error)) } } @@ -270,10 +266,8 @@ pub effect fn map( ) -> B ! E ? R { let completed = run result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => onSuccess(move success) - Failure { error } => run raise(move error) - } + Result.Success { value: success } => onSuccess(move success) + Result.Failure { error } => run raise(move error) } } @@ -289,10 +283,8 @@ pub effect fn mapError( ) -> A ! F ? R { let completed = run result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => move success - Failure { error } => run raise(onFailure(move error)) - } + Result.Success { value: success } => move success + Result.Failure { error } => run raise(onFailure(move error)) } } @@ -309,10 +301,8 @@ pub effect fn flatMap( ) -> B ! E | F ? R | S { let completed = run result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => run onSuccess(move success) - Failure { error } => run raise(move error) - } + Result.Success { value: success } => run onSuccess(move success) + Result.Failure { error } => run raise(move error) } } @@ -399,10 +389,8 @@ pub effect fn tap( ) -> A ! E | F ? R | S { let completed = run result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => run callback(move success) - Failure { error } => run raise(move error) - } + Result.Success { value: success } => run callback(move success) + Result.Failure { error } => run raise(move error) } } @@ -420,10 +408,8 @@ pub effect fn catchAll( ) -> A | B ! F ? R | S { let completed = run result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => move success - Failure { error } => run onFailure(move error) - } + Result.Success { value: success } => move success + Result.Failure { error } => run onFailure(move error) } } @@ -471,10 +457,8 @@ pub effect fn ensuring( let completed = run result(move self) let finalized = run move finalizer return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => move success - Failure { error } => run raise(move error) - } + Result.Success { value: success } => move success + Result.Failure { error } => run raise(move error) } } @@ -529,10 +513,8 @@ effect fn retryLoop( ) -> A ! E ? R { let completed = run result(self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => move success - Failure { error } => run retryFailure(self, move error, retries) - } + Result.Success { value: success } => move success + Result.Failure { error } => run retryFailure(self, move error, retries) } } @@ -691,10 +673,8 @@ pub effect fn provideEffect( where &mut P provides S from R { let completed = run acquireProvider(move self, acquire) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => move success - Failure { error } => run raise(move error) - } + Result.Success { value: success } => move success + Result.Failure { error } => run raise(move error) } } diff --git a/packages/compiler/stdlib/silk/filesystem.silk b/packages/compiler/stdlib/silk/filesystem.silk index 2a9a41d50..dfde06333 100644 --- a/packages/compiler/stdlib/silk/filesystem.silk +++ b/packages/compiler/stdlib/silk/filesystem.silk @@ -60,8 +60,8 @@ import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } import silk.effect { Effect } import silk.i32 as i32 -import silk.option { None, Option, Some, none, some } -import silk.result { Failure, Result, Success } +import silk.option { Option, none, some } +import silk.result { Result } import silk.string { InvalidUtf8, fromUtf8 as stringFromUtf8, @@ -280,10 +280,8 @@ fn containsNul(values: &[u8]) -> bool { fn validUtf8(values: &[u8]) -> bool { let decoded = stringFromUtf8(values) return match move decoded { - Result { value: outcome } => match move outcome { - Success { value: text } => true - Failure { error: invalid } => false - } + Result.Success { value: text } => true + Result.Failure { error: invalid } => false } } @@ -882,8 +880,8 @@ pub effect fn removeDirectoryRecursively( while usize.ZERO < vectorLength(&recorded) { let taken = vectorPop(&mut recorded) let emptied = match move taken { - Some { value: selected } => move selected - None {} => run fromBytes(rawBytes(path)) + Option.Some { value: selected } => move selected + Option.None => run fromBytes(rawBytes(path)) } let removed = run FileSystem.removeDirectory(&emptied) } @@ -903,14 +901,14 @@ fn classifyStatFailure( } fn classifyDirectory( - outcome: Success | Failure + outcome: Result ) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure { return match move outcome { - Success { value: info } => match move info { + Result.Success { value: info } => match move info { DirectoryInfo {} => DirectoryPresent {} FileInfo { byteLength } => DirectoryWrongType {} } - Failure { error: failure } => classifyStatFailure(move failure) + Result.Failure { error: failure } => classifyStatFailure(move failure) } } @@ -937,10 +935,7 @@ pub effect fn createDirectoriesRecursively( let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index) let prefix = run finishPath(move prefixBytes) let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix)) - let decision = match move completed { - Result { value: outcome } => - classifyDirectory(move outcome) - } + let decision = classifyDirectory(move completed) let ensured = match move decision { DirectoryPresent {} => () DirectoryMissing {} => run FileSystem.createDirectory(&prefix) @@ -984,10 +979,7 @@ pub effect fn writeFileWithParents( let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index) let prefix = run finishPath(move prefixBytes) let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix)) - let decision = match move completed { - Result { value: outcome } => - classifyDirectory(move outcome) - } + let decision = classifyDirectory(move completed) let ensured = match move decision { DirectoryPresent {} => () DirectoryMissing {} => run FileSystem.createDirectory(&prefix) @@ -1014,9 +1006,7 @@ effect fn existsFailure(failure: FileError) -> bool ! FileError { pub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem { let completed = run Intrinsic.effectResult(FileSystem.stat(path)) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: info } => true - Failure { error: failure } => run existsFailure(move failure) - } + Result.Success { value: info } => true + Result.Failure { error: failure } => run existsFailure(move failure) } } diff --git a/packages/compiler/stdlib/silk/format.silk b/packages/compiler/stdlib/silk/format.silk index 727ae2fb5..b9f735feb 100644 --- a/packages/compiler/stdlib/silk/format.silk +++ b/packages/compiler/stdlib/silk/format.silk @@ -71,8 +71,8 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option, Some, None } -import silk.result { Result, Success, Failure, failResult, succeed } +import silk.option { Option } +import silk.result { Result, failResult, succeed } import silk.string { String, make as stringMake, @@ -252,87 +252,81 @@ pub fn signedValue(text: string) -> Result { fn narrowU8(value: u64) -> Result { return match move u64.checkedToU8(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowU16(value: u64) -> Result { return match move u64.checkedToU16(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowU32(value: u64) -> Result { return match move u64.checkedToU32(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowUsize(value: u64) -> Result { return match move u64.checkedToUsize(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowI8(value: i64) -> Result { return match move i64.checkedToI8(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowI16(value: i64) -> Result { return match move i64.checkedToI16(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowI32(value: i64) -> Result { return match move i64.checkedToI32(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } fn narrowIsize(value: i64) -> Result { return match move i64.checkedToIsize(value) { - None {} => outOfRange() - Some { value: narrowed } => succeed(narrowed) + Option.None => outOfRange() + Option.Some { value: narrowed } => succeed(narrowed) } } /// Reads complete decimal text as a `u8`, rejecting a value above `u8.MAX`. pub fn u8Value(text: string) -> Result { return match move unsignedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowU8(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowU8(value) + Result.Failure { error } => failResult(move error) } } /// Reads complete decimal text as a `u16`, rejecting a value above `u16.MAX`. pub fn u16Value(text: string) -> Result { return match move unsignedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowU16(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowU16(value) + Result.Failure { error } => failResult(move error) } } /// Reads complete decimal text as a `u32`, rejecting a value above `u32.MAX`. pub fn u32Value(text: string) -> Result { return match move unsignedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowU32(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowU32(value) + Result.Failure { error } => failResult(move error) } } @@ -345,40 +339,32 @@ pub fn u64Value(text: string) -> Result { /// hold. pub fn usizeValue(text: string) -> Result { return match move unsignedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowUsize(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowUsize(value) + Result.Failure { error } => failResult(move error) } } /// Reads complete decimal text as an `i8`, rejecting a value outside `i8.MIN`–`i8.MAX`. pub fn i8Value(text: string) -> Result { return match move signedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowI8(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowI8(value) + Result.Failure { error } => failResult(move error) } } /// Reads complete decimal text as an `i16`, rejecting a value outside `i16.MIN`–`i16.MAX`. pub fn i16Value(text: string) -> Result { return match move signedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowI16(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowI16(value) + Result.Failure { error } => failResult(move error) } } /// Reads complete decimal text as an `i32`, rejecting a value outside `i32.MIN`–`i32.MAX`. pub fn i32Value(text: string) -> Result { return match move signedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowI32(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowI32(value) + Result.Failure { error } => failResult(move error) } } @@ -391,9 +377,7 @@ pub fn i64Value(text: string) -> Result { /// hold. pub fn isizeValue(text: string) -> Result { return match move signedValue(text) { - Result { value: outcome } => match move outcome { - Success { value } => narrowIsize(value) - Failure { error } => failResult(move error) - } + Result.Success { value } => narrowIsize(value) + Result.Failure { error } => failResult(move error) } } diff --git a/packages/compiler/stdlib/silk/hash_map.silk b/packages/compiler/stdlib/silk/hash_map.silk index 84c3b2044..cf1bd5a09 100644 --- a/packages/compiler/stdlib/silk/hash_map.silk +++ b/packages/compiler/stdlib/silk/hash_map.silk @@ -89,7 +89,7 @@ import silk.allocator { OutOfMemoryError } import silk.hash { HashKey, HashSeed } import silk.layout { Layout } import silk.layout { LayoutOverflow } -import silk.option { Option, Some, None } +import silk.option { Option, none, some } import silk.raw_buffer as RawBuffer import silk.slot as Slot import silk.u64 as u64 @@ -430,7 +430,7 @@ fn partValue(entry: Entry) -> Option { fn releaseKey(key: K, value: V) -> Option { drop key - return Option.some(move value) + return some(move value) } fn markRemoved(owned: &mut Table, index: usize) -> () { @@ -522,7 +522,7 @@ pub effect fn insert( if fresh { self.used = self.used + usize.ONE } - return Option.none() + return none() } /// Reports whether the map holds an entry under a key equivalent to one probe key. @@ -565,7 +565,7 @@ pub fn contains(self: &HashMap, key: K) -> bool { /// This function consumes the probe key. pub fn indexOf(self: &HashMap, key: K) -> Option { if self.capacity == usize.ZERO { - return Option.none() + return none() } let hashed = HashKey.hash(&key, &self.seed) let mut index = bucketOf(hashed, self.capacity) @@ -573,20 +573,20 @@ pub fn indexOf(self: &HashMap, key: K) -> Option { while scanned < self.capacity { let state = stateAt(self, index) if state == VACANT { - return Option.none() + return none() } if state == OCCUPIED { let held = entryAt(self, index) if held[usize.ZERO].hash == hashed { if (&held[usize.ZERO].key) == (&key) { - return Option.some(index) + return some(index) } } } index = advance(index, self.capacity) scanned = scanned + usize.ONE } - return Option.none() + return none() } /// Returns the value held under a key equivalent to one probe key, or an absent value. @@ -598,7 +598,7 @@ pub fn indexOf(self: &HashMap, key: K) -> Option { /// This function consumes the probe key and does not change the map. pub fn get(self: &HashMap, key: K) -> Option { if self.capacity == usize.ZERO { - return Option.none() + return none() } let hashed = HashKey.hash(&key, &self.seed) let mut index = bucketOf(hashed, self.capacity) @@ -606,7 +606,7 @@ pub fn get(self: &HashMap, key: K) -> Option(self, index) if state == VACANT { - return Option.none() + return none() } if state == OCCUPIED { let held = entryAt(self, index) @@ -619,7 +619,7 @@ pub fn get(self: &HashMap, key: K) -> Option() + return none() } /// Runs one take-once callback with exclusive access to an existing value. @@ -683,7 +683,7 @@ pub fn withMut< /// held is released, and the probe key is released as well. pub fn remove(self: &mut HashMap, key: K) -> Option { if self.capacity == usize.ZERO { - return Option.none() + return none() } let hashed = HashKey.hash(&key, &self.seed) let storage = Intrinsic.replace(self.storage, Unallocated { anchor: [] }) @@ -711,7 +711,7 @@ pub fn remove(self: &mut HashMap, key: K) -> Option { } if self.capacity <= found { self.storage = move owned - return Option.none() + return none() } let carried = evict(&mut owned, found) let marked = markRemoved(&mut owned, found) diff --git a/packages/compiler/stdlib/silk/hash_set.silk b/packages/compiler/stdlib/silk/hash_set.silk index b7b2fcb73..d743835cb 100644 --- a/packages/compiler/stdlib/silk/hash_set.silk +++ b/packages/compiler/stdlib/silk/hash_set.silk @@ -75,7 +75,7 @@ import silk.allocator { OutOfMemoryError } import silk.hash { HashKey, HashSeed } import silk.layout { Layout } import silk.layout { LayoutOverflow } -import silk.option { Option, Some, None } +import silk.option { Option, none, some } import silk.raw_buffer as RawBuffer import silk.slot as Slot import silk.u64 as u64 @@ -515,7 +515,7 @@ pub fn contains(self: &HashSet, value: T) -> bool { /// This function consumes the probe element. pub fn indexOf(self: &HashSet, value: T) -> Option { if self.capacity == usize.ZERO { - return Option.none() + return none() } let hashed = HashKey.hash(&value, &self.seed) let mut index = bucketOf(hashed, self.capacity) @@ -523,20 +523,20 @@ pub fn indexOf(self: &HashSet, value: T) -> Option { while scanned < self.capacity { let state = stateAt(self, index) if state == VACANT { - return Option.none() + return none() } if state == OCCUPIED { let held = memberAt(self, index) if held[usize.ZERO].hash == hashed { if (&held[usize.ZERO].value) == (&value) { - return Option.some(index) + return some(index) } } } index = advance(index, self.capacity) scanned = scanned + usize.ONE } - return Option.none() + return none() } /// Removes the element equivalent to one probe element and answers with it. @@ -546,7 +546,7 @@ pub fn indexOf(self: &HashSet, value: T) -> Option { /// Ownership passes to the caller; the set does not also release it. The probe element is released. pub fn remove(self: &mut HashSet, value: T) -> Option { if self.capacity == usize.ZERO { - return Option.none() + return none() } let hashed = HashKey.hash(&value, &self.seed) let storage = Intrinsic.replace(self.storage, Unseeded { anchor: [] }) @@ -574,13 +574,13 @@ pub fn remove(self: &mut HashSet, value: T) -> Option { } if self.capacity <= found { self.storage = move owned - return Option.none() + return none() } let carried = evict(&mut owned, found) let marked = markRemoved(&mut owned, found) self.storage = move owned self.length = self.length - usize.ONE - return Option.some(move carried) + return some(move carried) } fn evict(owned: &mut Slots, index: usize) -> T { diff --git a/packages/compiler/stdlib/silk/host_input.silk b/packages/compiler/stdlib/silk/host_input.silk index 36530fed5..5cbf0364a 100644 --- a/packages/compiler/stdlib/silk/host_input.silk +++ b/packages/compiler/stdlib/silk/host_input.silk @@ -93,7 +93,7 @@ import silk.bytes { Bytes } import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } -import silk.option { None, Option, Some } +import silk.option { Option } import silk.result { Result } import silk.string { InvalidUtf8, fromUtf8 as stringFromUtf8, utf8Bytes as stringUtf8Bytes } import silk.u8 as u8 @@ -235,8 +235,8 @@ pub effect fn arguments( while index < total { let found = run HostInput.argument(index) let owned = match move found { - Some { value: bytes } => move bytes - None {} => run missingArgument() + Option.Some { value: bytes } => move bytes + Option.None => run missingArgument() } let appended = run vectorAppend(&mut collected, move owned) index = index + usize.ONE diff --git a/packages/compiler/stdlib/silk/i16.silk b/packages/compiler/stdlib/silk/i16.silk index d2bf94061..4a365b2ca 100644 --- a/packages/compiler/stdlib/silk/i16.silk +++ b/packages/compiler/stdlib/silk/i16.silk @@ -37,7 +37,7 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -82,7 +82,7 @@ pub fn toU8(value: i16) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: i16) -> Option { - return Intrinsic.i16CheckedToU8(value) + return Intrinsic.i16CheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -94,7 +94,7 @@ pub fn toU16(value: i16) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: i16) -> Option { - return Intrinsic.i16CheckedToU16(value) + return Intrinsic.i16CheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -106,7 +106,7 @@ pub fn toU32(value: i16) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: i16) -> Option { - return Intrinsic.i16CheckedToU32(value) + return Intrinsic.i16CheckedToU32>(value, some, none) } /// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use @@ -118,7 +118,7 @@ pub fn toU64(value: i16) -> u64 { /// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU64(value: i16) -> Option { - return Intrinsic.i16CheckedToU64(value) + return Intrinsic.i16CheckedToU64>(value, some, none) } /// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use @@ -130,7 +130,7 @@ pub fn toUsize(value: i16) -> usize { /// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToUsize(value: i16) -> Option { - return Intrinsic.i16CheckedToUsize(value) + return Intrinsic.i16CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -142,7 +142,7 @@ pub fn toI8(value: i16) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: i16) -> Option { - return Intrinsic.i16CheckedToI8(value) + return Intrinsic.i16CheckedToI8>(value, some, none) } /// Returns `value` unchanged as `i16`. Use this function when generic conversion code @@ -154,7 +154,7 @@ pub fn toI16(value: i16) -> i16 { /// Returns `Some` with `value` unchanged as `i16`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToI16(value: i16) -> Option { - return Intrinsic.i16CheckedToI16(value) + return Intrinsic.i16CheckedToI16>(value, some, none) } /// Converts `value` exactly to `i32`. Every `i16` value is representable. @@ -165,7 +165,7 @@ pub fn toI32(value: i16) -> i32 { /// Converts `value` exactly to `i32` and returns `Some`. Every `i16` value is /// representable. pub fn checkedToI32(value: i16) -> Option { - return Intrinsic.i16CheckedToI32(value) + return Intrinsic.i16CheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `i16` value is representable. @@ -176,7 +176,7 @@ pub fn toI64(value: i16) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `i16` value is /// representable. pub fn checkedToI64(value: i16) -> Option { - return Intrinsic.i16CheckedToI64(value) + return Intrinsic.i16CheckedToI64>(value, some, none) } /// Converts `value` exactly to `isize`. Every `i16` value is representable. @@ -187,7 +187,7 @@ pub fn toIsize(value: i16) -> isize { /// Converts `value` exactly to `isize` and returns `Some`. Every `i16` value is /// representable. pub fn checkedToIsize(value: i16) -> Option { - return Intrinsic.i16CheckedToIsize(value) + return Intrinsic.i16CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -311,31 +311,31 @@ pub fn saturatingMultiply(left: i16, right: i16) -> i16 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `i16` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: i16, right: i16) -> Option { - return Intrinsic.i16CheckedAdd(left, right) + return Intrinsic.i16CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `i16` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: i16, right: i16) -> Option { - return Intrinsic.i16CheckedSubtract(left, right) + return Intrinsic.i16CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `i16` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: i16, right: i16) -> Option { - return Intrinsic.i16CheckedMultiply(left, right) + return Intrinsic.i16CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when an invalid quotient is input data. pub fn checkedDivide(left: i16, right: i16) -> Option { - return Intrinsic.i16CheckedDivide(left, right) + return Intrinsic.i16CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when invalid division is input data. pub fn checkedRemainder(left: i16, right: i16) -> Option { - return Intrinsic.i16CheckedRemainder(left, right) + return Intrinsic.i16CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/i32.silk b/packages/compiler/stdlib/silk/i32.silk index 68305537e..781bc6959 100644 --- a/packages/compiler/stdlib/silk/i32.silk +++ b/packages/compiler/stdlib/silk/i32.silk @@ -49,7 +49,7 @@ import silk.i16 as i16 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -94,7 +94,7 @@ pub fn toU8(value: i32) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: i32) -> Option { - return Intrinsic.i32CheckedToU8(value) + return Intrinsic.i32CheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -106,7 +106,7 @@ pub fn toU16(value: i32) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: i32) -> Option { - return Intrinsic.i32CheckedToU16(value) + return Intrinsic.i32CheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -118,7 +118,7 @@ pub fn toU32(value: i32) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: i32) -> Option { - return Intrinsic.i32CheckedToU32(value) + return Intrinsic.i32CheckedToU32>(value, some, none) } /// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use @@ -130,7 +130,7 @@ pub fn toU64(value: i32) -> u64 { /// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU64(value: i32) -> Option { - return Intrinsic.i32CheckedToU64(value) + return Intrinsic.i32CheckedToU64>(value, some, none) } /// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use @@ -142,7 +142,7 @@ pub fn toUsize(value: i32) -> usize { /// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToUsize(value: i32) -> Option { - return Intrinsic.i32CheckedToUsize(value) + return Intrinsic.i32CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -154,7 +154,7 @@ pub fn toI8(value: i32) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: i32) -> Option { - return Intrinsic.i32CheckedToI8(value) + return Intrinsic.i32CheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -166,7 +166,7 @@ pub fn toI16(value: i32) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: i32) -> Option { - return Intrinsic.i32CheckedToI16(value) + return Intrinsic.i32CheckedToI16>(value, some, none) } /// Returns `value` unchanged as `i32`. Use this function when generic conversion code @@ -178,7 +178,7 @@ pub fn toI32(value: i32) -> i32 { /// Returns `Some` with `value` unchanged as `i32`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToI32(value: i32) -> Option { - return Intrinsic.i32CheckedToI32(value) + return Intrinsic.i32CheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `i32` value is representable. @@ -189,7 +189,7 @@ pub fn toI64(value: i32) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `i32` value is /// representable. pub fn checkedToI64(value: i32) -> Option { - return Intrinsic.i32CheckedToI64(value) + return Intrinsic.i32CheckedToI64>(value, some, none) } /// Converts `value` exactly to `isize`. Every `i32` value is representable. @@ -200,7 +200,7 @@ pub fn toIsize(value: i32) -> isize { /// Converts `value` exactly to `isize` and returns `Some`. Every `i32` value is /// representable. pub fn checkedToIsize(value: i32) -> Option { - return Intrinsic.i32CheckedToIsize(value) + return Intrinsic.i32CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -324,31 +324,31 @@ pub fn saturatingMultiply(left: i32, right: i32) -> i32 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `i32` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: i32, right: i32) -> Option { - return Intrinsic.i32CheckedAdd(left, right) + return Intrinsic.i32CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `i32` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: i32, right: i32) -> Option { - return Intrinsic.i32CheckedSubtract(left, right) + return Intrinsic.i32CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `i32` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: i32, right: i32) -> Option { - return Intrinsic.i32CheckedMultiply(left, right) + return Intrinsic.i32CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when an invalid quotient is input data. pub fn checkedDivide(left: i32, right: i32) -> Option { - return Intrinsic.i32CheckedDivide(left, right) + return Intrinsic.i32CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when invalid division is input data. pub fn checkedRemainder(left: i32, right: i32) -> Option { - return Intrinsic.i32CheckedRemainder(left, right) + return Intrinsic.i32CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/i64.silk b/packages/compiler/stdlib/silk/i64.silk index ff6c2c4c7..15e732360 100644 --- a/packages/compiler/stdlib/silk/i64.silk +++ b/packages/compiler/stdlib/silk/i64.silk @@ -44,7 +44,7 @@ import silk.i16 as i16 import silk.i32 as i32 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -89,7 +89,7 @@ pub fn toU8(value: i64) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: i64) -> Option { - return Intrinsic.i64CheckedToU8(value) + return Intrinsic.i64CheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -101,7 +101,7 @@ pub fn toU16(value: i64) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: i64) -> Option { - return Intrinsic.i64CheckedToU16(value) + return Intrinsic.i64CheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -113,7 +113,7 @@ pub fn toU32(value: i64) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: i64) -> Option { - return Intrinsic.i64CheckedToU32(value) + return Intrinsic.i64CheckedToU32>(value, some, none) } /// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use @@ -125,7 +125,7 @@ pub fn toU64(value: i64) -> u64 { /// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU64(value: i64) -> Option { - return Intrinsic.i64CheckedToU64(value) + return Intrinsic.i64CheckedToU64>(value, some, none) } /// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use @@ -137,7 +137,7 @@ pub fn toUsize(value: i64) -> usize { /// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToUsize(value: i64) -> Option { - return Intrinsic.i64CheckedToUsize(value) + return Intrinsic.i64CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -149,7 +149,7 @@ pub fn toI8(value: i64) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: i64) -> Option { - return Intrinsic.i64CheckedToI8(value) + return Intrinsic.i64CheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -161,7 +161,7 @@ pub fn toI16(value: i64) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: i64) -> Option { - return Intrinsic.i64CheckedToI16(value) + return Intrinsic.i64CheckedToI16>(value, some, none) } /// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use @@ -173,7 +173,7 @@ pub fn toI32(value: i64) -> i32 { /// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI32(value: i64) -> Option { - return Intrinsic.i64CheckedToI32(value) + return Intrinsic.i64CheckedToI32>(value, some, none) } /// Returns `value` unchanged as `i64`. Use this function when generic conversion code @@ -185,7 +185,7 @@ pub fn toI64(value: i64) -> i64 { /// Returns `Some` with `value` unchanged as `i64`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToI64(value: i64) -> Option { - return Intrinsic.i64CheckedToI64(value) + return Intrinsic.i64CheckedToI64>(value, some, none) } /// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use @@ -197,7 +197,7 @@ pub fn toIsize(value: i64) -> isize { /// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToIsize(value: i64) -> Option { - return Intrinsic.i64CheckedToIsize(value) + return Intrinsic.i64CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -321,31 +321,31 @@ pub fn saturatingMultiply(left: i64, right: i64) -> i64 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `i64` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: i64, right: i64) -> Option { - return Intrinsic.i64CheckedAdd(left, right) + return Intrinsic.i64CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `i64` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: i64, right: i64) -> Option { - return Intrinsic.i64CheckedSubtract(left, right) + return Intrinsic.i64CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `i64` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: i64, right: i64) -> Option { - return Intrinsic.i64CheckedMultiply(left, right) + return Intrinsic.i64CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when an invalid quotient is input data. pub fn checkedDivide(left: i64, right: i64) -> Option { - return Intrinsic.i64CheckedDivide(left, right) + return Intrinsic.i64CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when invalid division is input data. pub fn checkedRemainder(left: i64, right: i64) -> Option { - return Intrinsic.i64CheckedRemainder(left, right) + return Intrinsic.i64CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/i8.silk b/packages/compiler/stdlib/silk/i8.silk index ea2cad5e2..1d9e7b092 100644 --- a/packages/compiler/stdlib/silk/i8.silk +++ b/packages/compiler/stdlib/silk/i8.silk @@ -42,7 +42,7 @@ import silk.i16 as i16 import silk.i32 as i32 import silk.i64 as i64 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -87,7 +87,7 @@ pub fn toU8(value: i8) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: i8) -> Option { - return Intrinsic.i8CheckedToU8(value) + return Intrinsic.i8CheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -99,7 +99,7 @@ pub fn toU16(value: i8) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: i8) -> Option { - return Intrinsic.i8CheckedToU16(value) + return Intrinsic.i8CheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -111,7 +111,7 @@ pub fn toU32(value: i8) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: i8) -> Option { - return Intrinsic.i8CheckedToU32(value) + return Intrinsic.i8CheckedToU32>(value, some, none) } /// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use @@ -123,7 +123,7 @@ pub fn toU64(value: i8) -> u64 { /// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU64(value: i8) -> Option { - return Intrinsic.i8CheckedToU64(value) + return Intrinsic.i8CheckedToU64>(value, some, none) } /// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use @@ -135,7 +135,7 @@ pub fn toUsize(value: i8) -> usize { /// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToUsize(value: i8) -> Option { - return Intrinsic.i8CheckedToUsize(value) + return Intrinsic.i8CheckedToUsize>(value, some, none) } /// Returns `value` unchanged as `i8`. Use this function when generic conversion code @@ -147,7 +147,7 @@ pub fn toI8(value: i8) -> i8 { /// Returns `Some` with `value` unchanged as `i8`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToI8(value: i8) -> Option { - return Intrinsic.i8CheckedToI8(value) + return Intrinsic.i8CheckedToI8>(value, some, none) } /// Converts `value` exactly to `i16`. Every `i8` value is representable. @@ -158,7 +158,7 @@ pub fn toI16(value: i8) -> i16 { /// Converts `value` exactly to `i16` and returns `Some`. Every `i8` value is /// representable. pub fn checkedToI16(value: i8) -> Option { - return Intrinsic.i8CheckedToI16(value) + return Intrinsic.i8CheckedToI16>(value, some, none) } /// Converts `value` exactly to `i32`. Every `i8` value is representable. @@ -169,7 +169,7 @@ pub fn toI32(value: i8) -> i32 { /// Converts `value` exactly to `i32` and returns `Some`. Every `i8` value is /// representable. pub fn checkedToI32(value: i8) -> Option { - return Intrinsic.i8CheckedToI32(value) + return Intrinsic.i8CheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `i8` value is representable. @@ -180,7 +180,7 @@ pub fn toI64(value: i8) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `i8` value is /// representable. pub fn checkedToI64(value: i8) -> Option { - return Intrinsic.i8CheckedToI64(value) + return Intrinsic.i8CheckedToI64>(value, some, none) } /// Converts `value` exactly to `isize`. Every `i8` value is representable. @@ -191,7 +191,7 @@ pub fn toIsize(value: i8) -> isize { /// Converts `value` exactly to `isize` and returns `Some`. Every `i8` value is /// representable. pub fn checkedToIsize(value: i8) -> Option { - return Intrinsic.i8CheckedToIsize(value) + return Intrinsic.i8CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -315,31 +315,31 @@ pub fn saturatingMultiply(left: i8, right: i8) -> i8 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `i8` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: i8, right: i8) -> Option { - return Intrinsic.i8CheckedAdd(left, right) + return Intrinsic.i8CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `i8` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: i8, right: i8) -> Option { - return Intrinsic.i8CheckedSubtract(left, right) + return Intrinsic.i8CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `i8` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: i8, right: i8) -> Option { - return Intrinsic.i8CheckedMultiply(left, right) + return Intrinsic.i8CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when an invalid quotient is input data. pub fn checkedDivide(left: i8, right: i8) -> Option { - return Intrinsic.i8CheckedDivide(left, right) + return Intrinsic.i8CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when invalid division is input data. pub fn checkedRemainder(left: i8, right: i8) -> Option { - return Intrinsic.i8CheckedRemainder(left, right) + return Intrinsic.i8CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/isize.silk b/packages/compiler/stdlib/silk/isize.silk index 661bddb29..dc2db72bb 100644 --- a/packages/compiler/stdlib/silk/isize.silk +++ b/packages/compiler/stdlib/silk/isize.silk @@ -41,7 +41,7 @@ import silk.i16 as i16 import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -95,7 +95,7 @@ pub fn toU8(value: isize) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: isize) -> Option { - return Intrinsic.isizeCheckedToU8(value) + return Intrinsic.isizeCheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -107,7 +107,7 @@ pub fn toU16(value: isize) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: isize) -> Option { - return Intrinsic.isizeCheckedToU16(value) + return Intrinsic.isizeCheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -119,7 +119,7 @@ pub fn toU32(value: isize) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: isize) -> Option { - return Intrinsic.isizeCheckedToU32(value) + return Intrinsic.isizeCheckedToU32>(value, some, none) } /// Converts `value` to `u64`. Traps if `value` is outside the `u64` range. Use @@ -131,7 +131,7 @@ pub fn toU64(value: isize) -> u64 { /// Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU64(value: isize) -> Option { - return Intrinsic.isizeCheckedToU64(value) + return Intrinsic.isizeCheckedToU64>(value, some, none) } /// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use @@ -143,7 +143,7 @@ pub fn toUsize(value: isize) -> usize { /// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToUsize(value: isize) -> Option { - return Intrinsic.isizeCheckedToUsize(value) + return Intrinsic.isizeCheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -155,7 +155,7 @@ pub fn toI8(value: isize) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: isize) -> Option { - return Intrinsic.isizeCheckedToI8(value) + return Intrinsic.isizeCheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -167,7 +167,7 @@ pub fn toI16(value: isize) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: isize) -> Option { - return Intrinsic.isizeCheckedToI16(value) + return Intrinsic.isizeCheckedToI16>(value, some, none) } /// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use @@ -179,7 +179,7 @@ pub fn toI32(value: isize) -> i32 { /// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI32(value: isize) -> Option { - return Intrinsic.isizeCheckedToI32(value) + return Intrinsic.isizeCheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `isize` value is representable. @@ -190,7 +190,7 @@ pub fn toI64(value: isize) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `isize` value is /// representable. pub fn checkedToI64(value: isize) -> Option { - return Intrinsic.isizeCheckedToI64(value) + return Intrinsic.isizeCheckedToI64>(value, some, none) } /// Returns `value` unchanged as `isize`. Use this function when generic conversion code @@ -202,7 +202,7 @@ pub fn toIsize(value: isize) -> isize { /// Returns `Some` with `value` unchanged as `isize`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToIsize(value: isize) -> Option { - return Intrinsic.isizeCheckedToIsize(value) + return Intrinsic.isizeCheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -326,31 +326,31 @@ pub fn saturatingMultiply(left: isize, right: isize) -> isize { /// Returns `Some` with `left + right`, or `None` if the result is outside the `isize` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: isize, right: isize) -> Option { - return Intrinsic.isizeCheckedAdd(left, right) + return Intrinsic.isizeCheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `isize` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: isize, right: isize) -> Option { - return Intrinsic.isizeCheckedSubtract(left, right) + return Intrinsic.isizeCheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `isize` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: isize, right: isize) -> Option { - return Intrinsic.isizeCheckedMultiply(left, right) + return Intrinsic.isizeCheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when an invalid quotient is input data. pub fn checkedDivide(left: isize, right: isize) -> Option { - return Intrinsic.isizeCheckedDivide(left, right) + return Intrinsic.isizeCheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`] is /// divided by `-1`. Use this function when invalid division is input data. pub fn checkedRemainder(left: isize, right: isize) -> Option { - return Intrinsic.isizeCheckedRemainder(left, right) + return Intrinsic.isizeCheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/layout.silk b/packages/compiler/stdlib/silk/layout.silk index eb964116b..84849cbd1 100644 --- a/packages/compiler/stdlib/silk/layout.silk +++ b/packages/compiler/stdlib/silk/layout.silk @@ -25,8 +25,7 @@ //! } //! ``` -import silk.option { None } -import silk.option { Some } +import silk.option { Option } import silk.usize as usize /// A byte size paired with a non-zero power-of-two alignment. @@ -78,7 +77,7 @@ pub fn make(size: usize, alignment: usize) -> Layout | InvalidAlignment { /// If the total byte size does not fit in `usize`, returns `LayoutOverflow`. pub fn repeat(layout: Layout, count: usize) -> Layout | LayoutOverflow { return match move usize.checkedMultiply(layout.bytes, count) { - None {} => LayoutOverflow {} - Some { value } => Layout { bytes: value, alignment: layout.alignment } + Option.None => LayoutOverflow {} + Option.Some { value } => Layout { bytes: value, alignment: layout.alignment } } } diff --git a/packages/compiler/stdlib/silk/local_scheduler.silk b/packages/compiler/stdlib/silk/local_scheduler.silk index 3d7b10bfc..c51e9f65b 100644 --- a/packages/compiler/stdlib/silk/local_scheduler.silk +++ b/packages/compiler/stdlib/silk/local_scheduler.silk @@ -14,8 +14,8 @@ import silk.execution as Execution import silk.fiber as Fiber import silk.hash as Hash import silk.hash_map as HashMap -import silk.option { None, Option, Some } -import silk.result { Failure, Result, Success } +import silk.option { Option } +import silk.result { Result } import silk.scheduler as Scheduler import silk.shared as Shared import silk.u64 as u64 @@ -196,10 +196,9 @@ fn publishTaskResult( result: Result, producer: Fiber.CompletionProducer, ) -> () { - let Result { value } = move result - return match move value { - Success { value: success } => Fiber.completeSuccess(move producer, move success) - Failure { error } => Fiber.completeFailure(move producer, move error) + return match move result { + Result.Success { value: success } => Fiber.completeSuccess(move producer, move success) + Result.Failure { error } => Fiber.completeFailure(move producer, move error) } } @@ -535,9 +534,8 @@ fn finishAdoption( response: Shared.Shared, outcome: Result, OutOfMemoryError>, ) -> () { - let Result, OutOfMemoryError> { value } = move outcome - return match move value { - Success> { value: previous } => finishAcceptedAdoption( + return match move outcome { + Result, OutOfMemoryError>.Success { value: previous } => finishAcceptedAdoption( move driver, parent, child, @@ -545,7 +543,7 @@ fn finishAdoption( move response, move previous, ) - Failure { error } => finishRejectedAdoption( + Result, OutOfMemoryError>.Failure { error } => finishRejectedAdoption( move response, move wake, move error, @@ -667,8 +665,8 @@ fn phaseCompleted(entry: &mut TaskEntry, output: &mut CompletionAudit) -> () { fn releaseEntry(entry: TaskEntry) -> () { drop entry return () } fn releaseRemoved(selected: Option) -> () { return match move selected { - None {} => () - Some { value } => releaseEntry(move value) + Option.None => () + Option.Some { value } => releaseEntry(move value) } } @@ -755,8 +753,8 @@ fn cancelEntry(entry: TaskEntry) -> () { fn cancelRemoved(selected: Option) -> () { return match move selected { - None {} => () - Some { value } => cancelEntry(move value) + Option.None => () + Option.Some { value } => cancelEntry(move value) } } @@ -1060,10 +1058,9 @@ effect fn finishRoot(selected: RootPending | RootReady) -> A ! E { } effect fn finishRootResult(result: Result) -> A ! E { - let Result { value } = move result - return match move value { - Success { value: success } => move success - Failure { error } => run raiseRoot(move error) + return match move result { + Result.Success { value: success } => move success + Result.Failure { error } => run raiseRoot(move error) } } diff --git a/packages/compiler/stdlib/silk/logger.silk b/packages/compiler/stdlib/silk/logger.silk index dbfa8c961..577fea8f2 100644 --- a/packages/compiler/stdlib/silk/logger.silk +++ b/packages/compiler/stdlib/silk/logger.silk @@ -51,7 +51,7 @@ import silk.bool as bool import silk.i32 as i32 -import silk.result { Failure, Result, Success } +import silk.result { Result } import silk.standard_streams { NativeStandardStreams, StreamWriteError, @@ -155,10 +155,8 @@ effect fn writeStdout( let bytes = stringUtf8Bytes(message) let completed = run Intrinsic.effectResult(writeStdoutCounted(&mut streams, bytes)) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: success } => () - Failure { error: failure } => run reject(3) - } + Result.Success { value: success } => () + Result.Failure { error: failure } => run reject(3) } } diff --git a/packages/compiler/stdlib/silk/option.silk b/packages/compiler/stdlib/silk/option.silk index 69b7dd81c..90132f6b6 100644 --- a/packages/compiler/stdlib/silk/option.silk +++ b/packages/compiler/stdlib/silk/option.silk @@ -6,7 +6,7 @@ //! when the caller is ready to consume the option. //! //! # Details -//! `Option` is the structural union of [`Some`] and [`None`]. Its combinators preserve affine +//! `Option` is a nominal union with `Some` and `None` variants. Its combinators preserve affine //! ownership: a present value moves forward, while an unused fallback or abandoned branch drops. //! //! # Examples @@ -37,39 +37,30 @@ //! } //! ``` -// Canonical recoverable outcome members. The compiler's Option is represented transparently by the -// structural union Some | None; the named declaration is its source-navigation anchor. - - - -/// The present member of [`Option`], carrying the available owned value. -pub struct Some { - /// The value moved through present-only combinator branches. - value: T -} - -/// The absent member of [`Option`]; it carries no explanation for the absence. -pub struct None {} - /// An owned value that is either [`Some`] or [`None`]. /// /// # Details /// /// Match on an `Option` when both arms need custom behavior. Prefer [`map`], [`flatMap`], or /// [`unwrapOr`] for the common transform, continue, and default cases. -pub struct Option { - /// The structural outcome narrowed by `match`. - value: Some | None +pub union Option { + /// The absent variant; it carries no explanation for the absence. + None, + /// The present variant, carrying the available owned value. + Some { + /// The value moved through present-only combinator branches. + value: T + } } /// Constructs an absent optional value of the requested element type. pub fn none() -> Option { - return None {} + return Option.None } /// Constructs a present option by moving `value` into it. pub fn some(value: T) -> Option { - return Some { value: move value } + return Option.Some { value: move value } } /// Applies `transform` once to a present value and keeps an absent value absent. @@ -80,8 +71,8 @@ pub fn some(value: T) -> Option { /// `match` instead when the original option must remain available. pub fn map(self: Option, transform: once fn(T) -> U) -> Option { return match move self { - Some { value } => some(transform(move value)) - None {} => none() + Option.Some { value } => some(transform(move value)) + Option.None => none() } } @@ -94,8 +85,8 @@ pub fn map(self: Option, transform: once fn(T) -> U) -> Option { /// reject the value without needing to explain why; use a `Result` when rejection needs an error. pub fn flatMap(self: Option, transform: once fn(T) -> Option) -> Option { return match move self { - Some { value } => transform(move value) - None {} => none() + Option.Some { value } => transform(move value) + Option.None => none() } } @@ -129,8 +120,8 @@ pub fn unwrapOr( fallback: T, ) -> T { return match move self { - Some { value } => keepPresent(move value, move fallback) - None {} => move fallback + Option.Some { value } => keepPresent(move value, move fallback) + Option.None => move fallback } } diff --git a/packages/compiler/stdlib/silk/os_child_process.silk b/packages/compiler/stdlib/silk/os_child_process.silk index 6db7926e0..9cea94258 100644 --- a/packages/compiler/stdlib/silk/os_child_process.silk +++ b/packages/compiler/stdlib/silk/os_child_process.silk @@ -67,7 +67,7 @@ import silk.child_process { import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } import silk.i32 as i32 -import silk.option { None, Option, Some, none } +import silk.option { Option, none } import silk.u32 as u32 import silk.u8 as u8 import silk.usize as usize @@ -172,8 +172,8 @@ effect fn drain(stream: i32, length: usize) -> Bytes ! ProcessError | OutOfMemor let mut output = bytesMutSlice(&mut buffer) let transferred = run rawCapture(stream, offset, move output, &mut lowReason, &mut nativeCode) let received = match move transferred { - None {} => run raise(captureOperation(), lowReason, nativeCode) - Some { value: selected } => selected + Option.None => run raise(captureOperation(), lowReason, nativeCode) + Option.Some { value: selected } => selected } if received == usize.ZERO { return run raise(captureOperation(), 10, u32.toU32(0)) } let view = bytesSlice(&buffer) diff --git a/packages/compiler/stdlib/silk/os_filesystem.silk b/packages/compiler/stdlib/silk/os_filesystem.silk index 5c3bc4edb..09bdb59ee 100644 --- a/packages/compiler/stdlib/silk/os_filesystem.silk +++ b/packages/compiler/stdlib/silk/os_filesystem.silk @@ -101,8 +101,8 @@ import silk.filesystem { view as pathView } import silk.i32 as i32 -import silk.option { None, Option, Some, none, some } -import silk.result { Failure, Result, Success } +import silk.option { Option, none, some } +import silk.result { Result } import silk.string { utf8Bytes as stringUtf8Bytes } import silk.u32 as u32 import silk.u8 as u8 @@ -219,8 +219,8 @@ effect fn openFile( let mut nativeCode = u32.toU32(0) let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode) return match move opened { - Some { value: handle } => move handle - None {} => run raise(move operation, lowReason, nativeCode) + Option.Some { value: handle } => move handle + Option.None => run raise(move operation, lowReason, nativeCode) } } @@ -233,8 +233,8 @@ effect fn openDirectory( let mut nativeCode = u32.toU32(0) let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode) return match move opened { - Some { value: handle } => move handle - None {} => run raise(move operation, lowReason, nativeCode) + Option.Some { value: handle } => move handle + Option.None => run raise(move operation, lowReason, nativeCode) } } @@ -248,10 +248,8 @@ effect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError { fn ignoreClose(result: Result<(), FileError>) -> () { return match move result { - Result<(), FileError> { value: outcome } => match move outcome { - Success<()> { value: completed } => () - Failure { error: failure } => () - } + Result<(), FileError>.Success { value: completed } => () + Result<(), FileError>.Failure { error: failure } => () } } @@ -289,8 +287,8 @@ effect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryErro received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode) } let length = match move received { - None {} => run raise(readFileOperation(), lowReason, nativeCode) - Some { value: selected } => selected + Option.None => run raise(readFileOperation(), lowReason, nativeCode) + Option.Some { value: selected } => selected } if length == usize.ZERO { complete = true @@ -315,17 +313,13 @@ effect fn readFile( let attempted = run Intrinsic.effectResult(readLoop(&mut handle)) let closed = run Intrinsic.effectResult(close(move handle, readFileOperation())) return match move attempted { - Result { value: outcome } => match move outcome { - Success { value: bytes } => match move closed { - Result<(), FileError> { value: closeOutcome } => match move closeOutcome { - Success<()> { value: completed } => move bytes - Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure) - } - } - Failure { error: primary } => match move primary { - FileError failure => run preserveFile(move failure, move closed) - OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed) - } + Result.Success { value: bytes } => match move closed { + Result<(), FileError>.Success { value: completed } => move bytes + Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure) + } + Result.Failure { error: primary } => match move primary { + FileError failure => run preserveFile(move failure, move closed) + OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed) } } } @@ -341,8 +335,8 @@ effect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError { written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode) } let length = match move written { - None {} => run raise(writeFileOperation(), lowReason, nativeCode) - Some { value: selected } => selected + Option.None => run raise(writeFileOperation(), lowReason, nativeCode) + Option.Some { value: selected } => selected } if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) } offset = offset + length @@ -355,15 +349,11 @@ effect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! let attempted = run Intrinsic.effectResult(writeLoop(&mut handle, bytes)) let closed = run Intrinsic.effectResult(close(move handle, writeFileOperation())) return match move attempted { - Result<(), FileError> { value: outcome } => match move outcome { - Success<()> { value: completed } => match move closed { - Result<(), FileError> { value: closeOutcome } => match move closeOutcome { - Success<()> { value: closedValue } => () - Failure { error: closeFailure } => run rerouteFile(move closeFailure) - } - } - Failure { error: primary } => run preserveFile(move primary, move closed) + Result<(), FileError>.Success { value: completed } => match move closed { + Result<(), FileError>.Success { value: closedValue } => () + Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure) } + Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed) } } @@ -397,8 +387,8 @@ effect fn listLoop( next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode) } let encodedLength = match move next { - Some { value: presentLength } => presentLength + usize.ONE - None {} => usize.ZERO + Option.Some { value: presentLength } => presentLength + usize.ONE + Option.None => usize.ZERO } if encodedLength == usize.ZERO { if lowReason == 8 { @@ -469,17 +459,13 @@ effect fn listDirectory( let attempted = run Intrinsic.effectResult(listLoop(&mut handle, path)) let closed = run Intrinsic.effectResult(close(move handle, listDirectoryOperation())) return match move attempted { - Result, FileError | OutOfMemoryError> { value: outcome } => match move outcome { - Success> { value: entries } => match move closed { - Result<(), FileError> { value: closeOutcome } => match move closeOutcome { - Success<()> { value: completed } => move entries - Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure) - } - } - Failure { error: primary } => match move primary { - FileError failure => run preserveFile(move failure, move closed) - OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed) - } + Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed { + Result<(), FileError>.Success { value: completed } => move entries + Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure) + } + Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary { + FileError failure => run preserveFile(move failure, move closed) + OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed) } } } @@ -550,8 +536,8 @@ effect fn createTemporaryDirectory( &mut nativeCode ) let length = match move chosen { - Some { value: selected } => selected - None {} => usize.ZERO + Option.Some { value: selected } => selected + Option.None => usize.ZERO } if length == usize.ZERO { if lowReason == 8 { @@ -575,8 +561,8 @@ effect fn createTemporaryDirectory( } } return match move created { - Some { value: path } => move path - None {} => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0)) + Option.Some { value: path } => move path + Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0)) } } diff --git a/packages/compiler/stdlib/silk/os_host_input.silk b/packages/compiler/stdlib/silk/os_host_input.silk index fe042272f..9d6c93d7c 100644 --- a/packages/compiler/stdlib/silk/os_host_input.silk +++ b/packages/compiler/stdlib/silk/os_host_input.silk @@ -46,7 +46,7 @@ import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } import silk.host_input { HostInput, HostInputError, inputFailure } import silk.i32 as i32 -import silk.option { None, Option, Some, none, some } +import silk.option { Option, none, some } import silk.string { utf8Bytes as stringUtf8Bytes } import silk.u32 as u32 import silk.u8 as u8 @@ -156,8 +156,8 @@ effect fn fetch( } } let encoded = match move received { - Some { value: total } => total + usize.ONE - None {} => usize.ZERO + Option.Some { value: total } => total + usize.ONE + Option.None => usize.ZERO } if encoded == usize.ZERO { if lowReason != 0 { return run raise() } @@ -213,8 +213,8 @@ effect fn workingDirectory( ) -> Bytes ! HostInputError | OutOfMemoryError ? &mut Allocator { let found = run fetch(2, usize.ZERO, stringUtf8Bytes("")) return match move found { - Some { value: bytes } => move bytes - None {} => run raise() + Option.Some { value: bytes } => move bytes + Option.None => run raise() } } diff --git a/packages/compiler/stdlib/silk/os_monotonic_clock.silk b/packages/compiler/stdlib/silk/os_monotonic_clock.silk index 178e25166..033d6c4a5 100644 --- a/packages/compiler/stdlib/silk/os_monotonic_clock.silk +++ b/packages/compiler/stdlib/silk/os_monotonic_clock.silk @@ -18,7 +18,7 @@ import silk.i64 as i64 import silk.monotonic_clock as MonotonicClock -import silk.option { None, Option, Some } +import silk.option { Option } import silk.system_clock as SystemClock import silk.system_clock { Instant } import silk.u64 as u64 @@ -43,8 +43,8 @@ fn invalidI64() -> i64 { fn requireI64(value: Option) -> i64 { return match move value { - None {} => invalidI64() - Some { value: present } => present + Option.None => invalidI64() + Option.Some { value: present } => present } } diff --git a/packages/compiler/stdlib/silk/os_standard_input.silk b/packages/compiler/stdlib/silk/os_standard_input.silk index b3bc8cb40..b8167cbce 100644 --- a/packages/compiler/stdlib/silk/os_standard_input.silk +++ b/packages/compiler/stdlib/silk/os_standard_input.silk @@ -32,7 +32,7 @@ //! ``` import silk.i32 as i32 -import silk.option { None, Option, Some, none } +import silk.option { Option, none } import silk.standard_input { ReadOutcome, StandardInput, @@ -90,8 +90,8 @@ effect fn read(self: &mut OsStandardInput, buffer: &mut [u8]) -> ReadOutcome ! S let mut nativeCode = u32.toU32(0) let received = run rawRead(move buffer, &mut lowReason, &mut nativeCode) let length = match move received { - None {} => run raise() - Some { value: selected } => selected + Option.None => run raise() + Option.Some { value: selected } => selected } if length == usize.ZERO { return endOfInput() } return filled(length) diff --git a/packages/compiler/stdlib/silk/result.silk b/packages/compiler/stdlib/silk/result.silk index 18ae7db41..40ead6468 100644 --- a/packages/compiler/stdlib/silk/result.silk +++ b/packages/compiler/stdlib/silk/result.silk @@ -33,10 +33,6 @@ //! let initial = Result.succeed(80) //! let halved = Result.flatMap(move initial, half) //! let answer = Result.map(move halved, addTwo) -//! let failed = Result.failResult(7) -//! if Result.isFailure(&failed) {} else { -//! return 0 -//! } //! return Result.unwrapOr(move answer, 0) //! } //! ``` @@ -46,18 +42,6 @@ import silk.bool as bool -/// The successful member of a completed [`Result`]. -pub struct Success { - /// The produced success value. - value: A -} - -/// The failed member of a completed [`Result`]. -pub struct Failure { - /// The produced failure value. - error: F -} - /// One completed outcome: either a success carrying `A` or a failure carrying `F`. /// /// # Details @@ -65,21 +49,29 @@ pub struct Failure { /// `Result` is the reified form of an Effect that has already run. Reifying an Effect turns its /// failure row into ordinary value data, which is what lets the failure combinators in /// `silk.effect` be written as ordinary Silk source instead of compiler built-ins. -/// A `Result` is consumed when matched or passed to a transforming combinator; borrow it for -/// [`isSuccess`] and [`isFailure`] when the payload must remain available. -pub struct Result { - /// The completed outcome, narrowed with `match`. - value: Success | Failure +/// A `Result` is consumed when matched or passed to a transforming combinator. Use a borrowed +/// match when the payload must remain available. +pub union Result { + /// A completed success. + Success { + /// The produced success value. + value: A + }, + /// A completed failure. + Failure { + /// The produced failure value. + error: F + } } /// Constructs a completed success by moving `value` into the success arm. pub fn succeed(value: A) -> Result { - return Result { value: Success { value: move value } } + return Result.Success { value: move value } } /// Constructs a completed failure by moving `error` into the failure arm. pub fn failResult(error: F) -> Result { - return Result { value: Failure { error: move error } } + return Result.Failure { error: move error } } /// Applies `transform` once to a success value and carries a failure through unchanged. @@ -90,10 +82,8 @@ pub fn failResult(error: F) -> Result { /// success type; use [`mapError`] to change the failure type instead. pub fn map(self: Result, transform: once fn(A) -> B) -> Result { return match move self { - Result { value: outcome } => match move outcome { - Success { value } => succeed(transform(move value)) - Failure { error } => failResult(move error) - } + Result.Success { value } => succeed(transform(move value)) + Result.Failure { error } => failResult(move error) } } @@ -105,10 +95,8 @@ pub fn map(self: Result, transform: once fn(A) -> B) -> Result(self: Result, transform: once fn(F) -> G) -> Result { return match move self { - Result { value: outcome } => match move outcome { - Success { value } => succeed(move value) - Failure { error } => failResult(transform(move error)) - } + Result.Success { value } => succeed(move value) + Result.Failure { error } => failResult(transform(move error)) } } @@ -121,10 +109,8 @@ pub fn mapError(self: Result, transform: once fn(F) -> G) -> Resu /// use [`mapError`] before or after this operation when the steps use different error types. pub fn flatMap(self: Result, transform: once fn(A) -> Result) -> Result { return match move self { - Result { value: outcome } => match move outcome { - Success { value } => transform(move value) - Failure { error } => failResult(move error) - } + Result.Success { value } => transform(move value) + Result.Failure { error } => failResult(move error) } } @@ -141,26 +127,8 @@ pub fn unwrapOr( fallback: A, ) -> A { return match move self { - Result { value: outcome } => match move outcome { - Success { value } => keepSuccess(move value, move fallback) - Failure { error } => keepFallback(move fallback, move error) - } - } -} - -/// Returns `true` when the borrowed outcome is [`Success`], without consuming either payload. -pub fn isSuccess(self: &Result) -> bool { - return match &self.value { - Success succeeded => true - Failure failed => false - } -} - -/// Returns `true` when the borrowed outcome is [`Failure`], without consuming either payload. -pub fn isFailure(self: &Result) -> bool { - return match &self.value { - Success succeeded => false - Failure failed => true + Result.Success { value } => keepSuccess(move value, move fallback) + Result.Failure { error } => keepFallback(move fallback, move error) } } diff --git a/packages/compiler/stdlib/silk/string.silk b/packages/compiler/stdlib/silk/string.silk index 0e8cb892f..4d54bce8a 100644 --- a/packages/compiler/stdlib/silk/string.silk +++ b/packages/compiler/stdlib/silk/string.silk @@ -26,8 +26,9 @@ //! let valid = String.fromUtf8(b"Silk") //! |> Result.unwrapOr("") //! let invalid = String.fromUtf8(b"a\x80") -//! if !Result.isFailure(&invalid) { -//! return 0 +//! match move invalid { +//! Result.Result.Success { .. } => return 0 +//! Result.Result.Failure { .. } => () //! } //! let length = String.byteLength(valid) //! |> usize.toI32 @@ -92,9 +93,7 @@ import silk.char as char import silk.char { fromU32 as charFromU32 } import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } -import silk.option { None } import silk.option { Option, none, some } -import silk.option { Some } import silk.result { Result, failResult, succeed } import silk.u32 as u32 import silk.u8 as u8 @@ -208,8 +207,8 @@ pub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string { /// first invalid byte offset in [`InvalidUtf8`]. pub fn fromUtf8(values: &[u8]) -> Result { let failure = match move firstInvalidUtf8(values) { - Some { value } => move value - None {} => values.length + usize.ONE + Option.Some { value } => move value + Option.None => values.length + usize.ONE } if failure <= values.length { return failResult(InvalidUtf8 { offset: failure }) @@ -244,8 +243,8 @@ pub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator /// Effect failure channel. No owned string is returned in either failure case. pub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator { let failure = match move firstInvalidUtf8(values) { - Some { value } => move value - None {} => values.length + usize.ONE + Option.Some { value } => move value + Option.None => values.length + usize.ONE } if failure <= values.length { return failResult(InvalidUtf8 { offset: failure }) @@ -382,11 +381,11 @@ pub fn nextScalar(value: string, cursor: ScalarCursor) -> Option { } } return match move charFromU32(scalar) { - Some { value: decoded } => some(ScalarStep { + Option.Some { value: decoded } => some(ScalarStep { scalar: decoded, byteOffset: offset, next: ScalarCursor { byteOffset: offset + width } }) - None {} => none() + Option.None => none() } } diff --git a/packages/compiler/stdlib/silk/u16.silk b/packages/compiler/stdlib/silk/u16.silk index 4efeae51a..a0f32b754 100644 --- a/packages/compiler/stdlib/silk/u16.silk +++ b/packages/compiler/stdlib/silk/u16.silk @@ -41,7 +41,7 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u32 as u32 @@ -67,7 +67,7 @@ pub fn toU8(value: u16) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: u16) -> Option { - return Intrinsic.u16CheckedToU8(value) + return Intrinsic.u16CheckedToU8>(value, some, none) } /// Returns `value` unchanged as `u16`. Use this function when generic conversion code @@ -79,7 +79,7 @@ pub fn toU16(value: u16) -> u16 { /// Returns `Some` with `value` unchanged as `u16`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToU16(value: u16) -> Option { - return Intrinsic.u16CheckedToU16(value) + return Intrinsic.u16CheckedToU16>(value, some, none) } /// Converts `value` exactly to `u32`. Every `u16` value is representable. @@ -90,7 +90,7 @@ pub fn toU32(value: u16) -> u32 { /// Converts `value` exactly to `u32` and returns `Some`. Every `u16` value is /// representable. pub fn checkedToU32(value: u16) -> Option { - return Intrinsic.u16CheckedToU32(value) + return Intrinsic.u16CheckedToU32>(value, some, none) } /// Converts `value` exactly to `u64`. Every `u16` value is representable. @@ -101,7 +101,7 @@ pub fn toU64(value: u16) -> u64 { /// Converts `value` exactly to `u64` and returns `Some`. Every `u16` value is /// representable. pub fn checkedToU64(value: u16) -> Option { - return Intrinsic.u16CheckedToU64(value) + return Intrinsic.u16CheckedToU64>(value, some, none) } /// Converts `value` exactly to `usize`. Every `u16` value is representable. @@ -112,7 +112,7 @@ pub fn toUsize(value: u16) -> usize { /// Converts `value` exactly to `usize` and returns `Some`. Every `u16` value is /// representable. pub fn checkedToUsize(value: u16) -> Option { - return Intrinsic.u16CheckedToUsize(value) + return Intrinsic.u16CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -124,7 +124,7 @@ pub fn toI8(value: u16) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: u16) -> Option { - return Intrinsic.u16CheckedToI8(value) + return Intrinsic.u16CheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -136,7 +136,7 @@ pub fn toI16(value: u16) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: u16) -> Option { - return Intrinsic.u16CheckedToI16(value) + return Intrinsic.u16CheckedToI16>(value, some, none) } /// Converts `value` exactly to `i32`. Every `u16` value is representable. @@ -147,7 +147,7 @@ pub fn toI32(value: u16) -> i32 { /// Converts `value` exactly to `i32` and returns `Some`. Every `u16` value is /// representable. pub fn checkedToI32(value: u16) -> Option { - return Intrinsic.u16CheckedToI32(value) + return Intrinsic.u16CheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `u16` value is representable. @@ -158,7 +158,7 @@ pub fn toI64(value: u16) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `u16` value is /// representable. pub fn checkedToI64(value: u16) -> Option { - return Intrinsic.u16CheckedToI64(value) + return Intrinsic.u16CheckedToI64>(value, some, none) } /// Converts `value` exactly to `isize`. Every `u16` value is representable. @@ -169,7 +169,7 @@ pub fn toIsize(value: u16) -> isize { /// Converts `value` exactly to `isize` and returns `Some`. Every `u16` value is /// representable. pub fn checkedToIsize(value: u16) -> Option { - return Intrinsic.u16CheckedToIsize(value) + return Intrinsic.u16CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -292,31 +292,31 @@ pub fn saturatingMultiply(left: u16, right: u16) -> u16 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `u16` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: u16, right: u16) -> Option { - return Intrinsic.u16CheckedAdd(left, right) + return Intrinsic.u16CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `u16` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: u16, right: u16) -> Option { - return Intrinsic.u16CheckedSubtract(left, right) + return Intrinsic.u16CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `u16` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: u16, right: u16) -> Option { - return Intrinsic.u16CheckedMultiply(left, right) + return Intrinsic.u16CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this /// function when a zero divisor is input data. pub fn checkedDivide(left: u16, right: u16) -> Option { - return Intrinsic.u16CheckedDivide(left, right) + return Intrinsic.u16CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function /// when a zero divisor is input data. pub fn checkedRemainder(left: u16, right: u16) -> Option { - return Intrinsic.u16CheckedRemainder(left, right) + return Intrinsic.u16CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/u32.silk b/packages/compiler/stdlib/silk/u32.silk index 155813fdf..0a28e152f 100644 --- a/packages/compiler/stdlib/silk/u32.silk +++ b/packages/compiler/stdlib/silk/u32.silk @@ -36,7 +36,7 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -62,7 +62,7 @@ pub fn toU8(value: u32) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: u32) -> Option { - return Intrinsic.u32CheckedToU8(value) + return Intrinsic.u32CheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -74,7 +74,7 @@ pub fn toU16(value: u32) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: u32) -> Option { - return Intrinsic.u32CheckedToU16(value) + return Intrinsic.u32CheckedToU16>(value, some, none) } /// Returns `value` unchanged as `u32`. Use this function when generic conversion code @@ -86,7 +86,7 @@ pub fn toU32(value: u32) -> u32 { /// Returns `Some` with `value` unchanged as `u32`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToU32(value: u32) -> Option { - return Intrinsic.u32CheckedToU32(value) + return Intrinsic.u32CheckedToU32>(value, some, none) } /// Converts `value` exactly to `u64`. Every `u32` value is representable. @@ -97,7 +97,7 @@ pub fn toU64(value: u32) -> u64 { /// Converts `value` exactly to `u64` and returns `Some`. Every `u32` value is /// representable. pub fn checkedToU64(value: u32) -> Option { - return Intrinsic.u32CheckedToU64(value) + return Intrinsic.u32CheckedToU64>(value, some, none) } /// Converts `value` exactly to `usize`. Every `u32` value is representable. @@ -108,7 +108,7 @@ pub fn toUsize(value: u32) -> usize { /// Converts `value` exactly to `usize` and returns `Some`. Every `u32` value is /// representable. pub fn checkedToUsize(value: u32) -> Option { - return Intrinsic.u32CheckedToUsize(value) + return Intrinsic.u32CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -120,7 +120,7 @@ pub fn toI8(value: u32) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: u32) -> Option { - return Intrinsic.u32CheckedToI8(value) + return Intrinsic.u32CheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -132,7 +132,7 @@ pub fn toI16(value: u32) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: u32) -> Option { - return Intrinsic.u32CheckedToI16(value) + return Intrinsic.u32CheckedToI16>(value, some, none) } /// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use @@ -144,7 +144,7 @@ pub fn toI32(value: u32) -> i32 { /// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI32(value: u32) -> Option { - return Intrinsic.u32CheckedToI32(value) + return Intrinsic.u32CheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `u32` value is representable. @@ -155,7 +155,7 @@ pub fn toI64(value: u32) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `u32` value is /// representable. pub fn checkedToI64(value: u32) -> Option { - return Intrinsic.u32CheckedToI64(value) + return Intrinsic.u32CheckedToI64>(value, some, none) } /// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use @@ -167,7 +167,7 @@ pub fn toIsize(value: u32) -> isize { /// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToIsize(value: u32) -> Option { - return Intrinsic.u32CheckedToIsize(value) + return Intrinsic.u32CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -290,31 +290,31 @@ pub fn saturatingMultiply(left: u32, right: u32) -> u32 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `u32` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: u32, right: u32) -> Option { - return Intrinsic.u32CheckedAdd(left, right) + return Intrinsic.u32CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `u32` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: u32, right: u32) -> Option { - return Intrinsic.u32CheckedSubtract(left, right) + return Intrinsic.u32CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `u32` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: u32, right: u32) -> Option { - return Intrinsic.u32CheckedMultiply(left, right) + return Intrinsic.u32CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this /// function when a zero divisor is input data. pub fn checkedDivide(left: u32, right: u32) -> Option { - return Intrinsic.u32CheckedDivide(left, right) + return Intrinsic.u32CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function /// when a zero divisor is input data. pub fn checkedRemainder(left: u32, right: u32) -> Option { - return Intrinsic.u32CheckedRemainder(left, right) + return Intrinsic.u32CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/u64.silk b/packages/compiler/stdlib/silk/u64.silk index 0b856f7b3..f48c8b3cf 100644 --- a/packages/compiler/stdlib/silk/u64.silk +++ b/packages/compiler/stdlib/silk/u64.silk @@ -39,7 +39,7 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -65,7 +65,7 @@ pub fn toU8(value: u64) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: u64) -> Option { - return Intrinsic.u64CheckedToU8(value) + return Intrinsic.u64CheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -77,7 +77,7 @@ pub fn toU16(value: u64) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: u64) -> Option { - return Intrinsic.u64CheckedToU16(value) + return Intrinsic.u64CheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -89,7 +89,7 @@ pub fn toU32(value: u64) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: u64) -> Option { - return Intrinsic.u64CheckedToU32(value) + return Intrinsic.u64CheckedToU32>(value, some, none) } /// Returns `value` unchanged as `u64`. Use this function when generic conversion code @@ -101,7 +101,7 @@ pub fn toU64(value: u64) -> u64 { /// Returns `Some` with `value` unchanged as `u64`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToU64(value: u64) -> Option { - return Intrinsic.u64CheckedToU64(value) + return Intrinsic.u64CheckedToU64>(value, some, none) } /// Converts `value` to `usize`. Traps if `value` is outside the `usize` range. Use @@ -113,7 +113,7 @@ pub fn toUsize(value: u64) -> usize { /// Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToUsize(value: u64) -> Option { - return Intrinsic.u64CheckedToUsize(value) + return Intrinsic.u64CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -125,7 +125,7 @@ pub fn toI8(value: u64) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: u64) -> Option { - return Intrinsic.u64CheckedToI8(value) + return Intrinsic.u64CheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -137,7 +137,7 @@ pub fn toI16(value: u64) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: u64) -> Option { - return Intrinsic.u64CheckedToI16(value) + return Intrinsic.u64CheckedToI16>(value, some, none) } /// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use @@ -149,7 +149,7 @@ pub fn toI32(value: u64) -> i32 { /// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI32(value: u64) -> Option { - return Intrinsic.u64CheckedToI32(value) + return Intrinsic.u64CheckedToI32>(value, some, none) } /// Converts `value` to `i64`. Traps if `value` is outside the `i64` range. Use @@ -161,7 +161,7 @@ pub fn toI64(value: u64) -> i64 { /// Converts `value` to `i64`, or returns `None` if `value` is outside the `i64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI64(value: u64) -> Option { - return Intrinsic.u64CheckedToI64(value) + return Intrinsic.u64CheckedToI64>(value, some, none) } /// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use @@ -173,7 +173,7 @@ pub fn toIsize(value: u64) -> isize { /// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToIsize(value: u64) -> Option { - return Intrinsic.u64CheckedToIsize(value) + return Intrinsic.u64CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -296,31 +296,31 @@ pub fn saturatingMultiply(left: u64, right: u64) -> u64 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `u64` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: u64, right: u64) -> Option { - return Intrinsic.u64CheckedAdd(left, right) + return Intrinsic.u64CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `u64` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: u64, right: u64) -> Option { - return Intrinsic.u64CheckedSubtract(left, right) + return Intrinsic.u64CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `u64` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: u64, right: u64) -> Option { - return Intrinsic.u64CheckedMultiply(left, right) + return Intrinsic.u64CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this /// function when a zero divisor is input data. pub fn checkedDivide(left: u64, right: u64) -> Option { - return Intrinsic.u64CheckedDivide(left, right) + return Intrinsic.u64CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function /// when a zero divisor is input data. pub fn checkedRemainder(left: u64, right: u64) -> Option { - return Intrinsic.u64CheckedRemainder(left, right) + return Intrinsic.u64CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/u8.silk b/packages/compiler/stdlib/silk/u8.silk index 3ec668c5a..53fc82008 100644 --- a/packages/compiler/stdlib/silk/u8.silk +++ b/packages/compiler/stdlib/silk/u8.silk @@ -51,7 +51,7 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -77,7 +77,7 @@ pub fn toU8(value: u8) -> u8 { /// Returns `Some` with `value` unchanged as `u8`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToU8(value: u8) -> Option { - return Intrinsic.u8CheckedToU8(value) + return Intrinsic.u8CheckedToU8>(value, some, none) } /// Converts `value` exactly to `u16`. Every `u8` value is representable. @@ -88,7 +88,7 @@ pub fn toU16(value: u8) -> u16 { /// Converts `value` exactly to `u16` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToU16(value: u8) -> Option { - return Intrinsic.u8CheckedToU16(value) + return Intrinsic.u8CheckedToU16>(value, some, none) } /// Converts `value` exactly to `u32`. Every `u8` value is representable. @@ -99,7 +99,7 @@ pub fn toU32(value: u8) -> u32 { /// Converts `value` exactly to `u32` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToU32(value: u8) -> Option { - return Intrinsic.u8CheckedToU32(value) + return Intrinsic.u8CheckedToU32>(value, some, none) } /// Converts `value` exactly to `u64`. Every `u8` value is representable. @@ -110,7 +110,7 @@ pub fn toU64(value: u8) -> u64 { /// Converts `value` exactly to `u64` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToU64(value: u8) -> Option { - return Intrinsic.u8CheckedToU64(value) + return Intrinsic.u8CheckedToU64>(value, some, none) } /// Converts `value` exactly to `usize`. Every `u8` value is representable. @@ -121,7 +121,7 @@ pub fn toUsize(value: u8) -> usize { /// Converts `value` exactly to `usize` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToUsize(value: u8) -> Option { - return Intrinsic.u8CheckedToUsize(value) + return Intrinsic.u8CheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -133,7 +133,7 @@ pub fn toI8(value: u8) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: u8) -> Option { - return Intrinsic.u8CheckedToI8(value) + return Intrinsic.u8CheckedToI8>(value, some, none) } /// Converts `value` exactly to `i16`. Every `u8` value is representable. @@ -144,7 +144,7 @@ pub fn toI16(value: u8) -> i16 { /// Converts `value` exactly to `i16` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToI16(value: u8) -> Option { - return Intrinsic.u8CheckedToI16(value) + return Intrinsic.u8CheckedToI16>(value, some, none) } /// Converts `value` exactly to `i32`. Every `u8` value is representable. @@ -155,7 +155,7 @@ pub fn toI32(value: u8) -> i32 { /// Converts `value` exactly to `i32` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToI32(value: u8) -> Option { - return Intrinsic.u8CheckedToI32(value) + return Intrinsic.u8CheckedToI32>(value, some, none) } /// Converts `value` exactly to `i64`. Every `u8` value is representable. @@ -166,7 +166,7 @@ pub fn toI64(value: u8) -> i64 { /// Converts `value` exactly to `i64` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToI64(value: u8) -> Option { - return Intrinsic.u8CheckedToI64(value) + return Intrinsic.u8CheckedToI64>(value, some, none) } /// Converts `value` exactly to `isize`. Every `u8` value is representable. @@ -177,7 +177,7 @@ pub fn toIsize(value: u8) -> isize { /// Converts `value` exactly to `isize` and returns `Some`. Every `u8` value is /// representable. pub fn checkedToIsize(value: u8) -> Option { - return Intrinsic.u8CheckedToIsize(value) + return Intrinsic.u8CheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -300,31 +300,31 @@ pub fn saturatingMultiply(left: u8, right: u8) -> u8 { /// Returns `Some` with `left + right`, or `None` if the result is outside the `u8` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: u8, right: u8) -> Option { - return Intrinsic.u8CheckedAdd(left, right) + return Intrinsic.u8CheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `u8` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: u8, right: u8) -> Option { - return Intrinsic.u8CheckedSubtract(left, right) + return Intrinsic.u8CheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `u8` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: u8, right: u8) -> Option { - return Intrinsic.u8CheckedMultiply(left, right) + return Intrinsic.u8CheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this /// function when a zero divisor is input data. pub fn checkedDivide(left: u8, right: u8) -> Option { - return Intrinsic.u8CheckedDivide(left, right) + return Intrinsic.u8CheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function /// when a zero divisor is input data. pub fn checkedRemainder(left: u8, right: u8) -> Option { - return Intrinsic.u8CheckedRemainder(left, right) + return Intrinsic.u8CheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/unicode.silk b/packages/compiler/stdlib/silk/unicode.silk index 6342bb9a1..bf52103d4 100644 --- a/packages/compiler/stdlib/silk/unicode.silk +++ b/packages/compiler/stdlib/silk/unicode.silk @@ -70,7 +70,7 @@ import silk.bool as bool import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } -import silk.result { Result, Success, Failure } +import silk.result { Result } import silk.string { String, InvalidUtf8, copyUtf8, make as stringMake, utf8Bytes } import silk.u32 as u32 import silk.u8 as u8 @@ -360,10 +360,8 @@ effect fn encodeOwned(scalars: Vector) -> String ! OutOfMemoryError ? &mut } let owned = run copyUtf8(vectorAsSlice(&bytes)) return match move owned { - Result { value: outcome } => match move outcome { - Success { value } => move value - Failure { error } => stringMake() - } + Result.Success { value } => move value + Result.Failure { error } => stringMake() } } diff --git a/packages/compiler/stdlib/silk/usize.silk b/packages/compiler/stdlib/silk/usize.silk index 7369f20bb..0c452ca7b 100644 --- a/packages/compiler/stdlib/silk/usize.silk +++ b/packages/compiler/stdlib/silk/usize.silk @@ -43,7 +43,7 @@ import silk.i32 as i32 import silk.i64 as i64 import silk.i8 as i8 import silk.isize as isize -import silk.option { Option } +import silk.option { Option, none, some } import silk.result { Result } import silk.string { String } import silk.u16 as u16 @@ -80,7 +80,7 @@ pub fn toU8(value: usize) -> u8 { /// Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU8(value: usize) -> Option { - return Intrinsic.usizeCheckedToU8(value) + return Intrinsic.usizeCheckedToU8>(value, some, none) } /// Converts `value` to `u16`. Traps if `value` is outside the `u16` range. Use @@ -92,7 +92,7 @@ pub fn toU16(value: usize) -> u16 { /// Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU16(value: usize) -> Option { - return Intrinsic.usizeCheckedToU16(value) + return Intrinsic.usizeCheckedToU16>(value, some, none) } /// Converts `value` to `u32`. Traps if `value` is outside the `u32` range. Use @@ -104,7 +104,7 @@ pub fn toU32(value: usize) -> u32 { /// Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToU32(value: usize) -> Option { - return Intrinsic.usizeCheckedToU32(value) + return Intrinsic.usizeCheckedToU32>(value, some, none) } /// Converts `value` exactly to `u64`. Every `usize` value is representable. @@ -115,7 +115,7 @@ pub fn toU64(value: usize) -> u64 { /// Converts `value` exactly to `u64` and returns `Some`. Every `usize` value is /// representable. pub fn checkedToU64(value: usize) -> Option { - return Intrinsic.usizeCheckedToU64(value) + return Intrinsic.usizeCheckedToU64>(value, some, none) } /// Returns `value` unchanged as `usize`. Use this function when generic conversion code @@ -127,7 +127,7 @@ pub fn toUsize(value: usize) -> usize { /// Returns `Some` with `value` unchanged as `usize`. Use this function when generic /// checked-conversion code can select the same source and destination type. pub fn checkedToUsize(value: usize) -> Option { - return Intrinsic.usizeCheckedToUsize(value) + return Intrinsic.usizeCheckedToUsize>(value, some, none) } /// Converts `value` to `i8`. Traps if `value` is outside the `i8` range. Use @@ -139,7 +139,7 @@ pub fn toI8(value: usize) -> i8 { /// Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI8(value: usize) -> Option { - return Intrinsic.usizeCheckedToI8(value) + return Intrinsic.usizeCheckedToI8>(value, some, none) } /// Converts `value` to `i16`. Traps if `value` is outside the `i16` range. Use @@ -151,7 +151,7 @@ pub fn toI16(value: usize) -> i16 { /// Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI16(value: usize) -> Option { - return Intrinsic.usizeCheckedToI16(value) + return Intrinsic.usizeCheckedToI16>(value, some, none) } /// Converts `value` to `i32`. Traps if `value` is outside the `i32` range. Use @@ -163,7 +163,7 @@ pub fn toI32(value: usize) -> i32 { /// Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI32(value: usize) -> Option { - return Intrinsic.usizeCheckedToI32(value) + return Intrinsic.usizeCheckedToI32>(value, some, none) } /// Converts `value` to `i64`. Traps if `value` is outside the `i64` range. Use @@ -175,7 +175,7 @@ pub fn toI64(value: usize) -> i64 { /// Converts `value` to `i64`, or returns `None` if `value` is outside the `i64` /// range. Use this function when an out-of-range value is input data. pub fn checkedToI64(value: usize) -> Option { - return Intrinsic.usizeCheckedToI64(value) + return Intrinsic.usizeCheckedToI64>(value, some, none) } /// Converts `value` to `isize`. Traps if `value` is outside the `isize` range. Use @@ -187,7 +187,7 @@ pub fn toIsize(value: usize) -> isize { /// Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` /// range. Use this function when an out-of-range value is input data. pub fn checkedToIsize(value: usize) -> Option { - return Intrinsic.usizeCheckedToIsize(value) + return Intrinsic.usizeCheckedToIsize>(value, some, none) } /// Converts `value` to the nearest `f32` value, with ties to even. @@ -310,31 +310,31 @@ pub fn saturatingMultiply(left: usize, right: usize) -> usize { /// Returns `Some` with `left + right`, or `None` if the result is outside the `usize` range. /// Use this function when overflow is input data. pub fn checkedAdd(left: usize, right: usize) -> Option { - return Intrinsic.usizeCheckedAdd(left, right) + return Intrinsic.usizeCheckedAdd>(left, right, some, none) } /// Returns `Some` with `left - right`, or `None` if the result is outside the `usize` range. /// Use this function when overflow is input data. pub fn checkedSubtract(left: usize, right: usize) -> Option { - return Intrinsic.usizeCheckedSubtract(left, right) + return Intrinsic.usizeCheckedSubtract>(left, right, some, none) } /// Returns `Some` with `left * right`, or `None` if the result is outside the `usize` range. /// Use this function when overflow is input data. pub fn checkedMultiply(left: usize, right: usize) -> Option { - return Intrinsic.usizeCheckedMultiply(left, right) + return Intrinsic.usizeCheckedMultiply>(left, right, some, none) } /// Returns `Some` with `left / right`, or `None` if `right` is zero. Use this /// function when a zero divisor is input data. pub fn checkedDivide(left: usize, right: usize) -> Option { - return Intrinsic.usizeCheckedDivide(left, right) + return Intrinsic.usizeCheckedDivide>(left, right, some, none) } /// Returns `Some` with the remainder, or `None` if `right` is zero. Use this function /// when a zero divisor is input data. pub fn checkedRemainder(left: usize, right: usize) -> Option { - return Intrinsic.usizeCheckedRemainder(left, right) + return Intrinsic.usizeCheckedRemainder>(left, right, some, none) } /// Returns `true` when `left` and `right` are equal. diff --git a/packages/compiler/stdlib/silk/vector.silk b/packages/compiler/stdlib/silk/vector.silk index 1dfbe52ac..24ba1d635 100644 --- a/packages/compiler/stdlib/silk/vector.silk +++ b/packages/compiler/stdlib/silk/vector.silk @@ -107,7 +107,7 @@ import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } import silk.layout { Layout } import silk.layout { LayoutOverflow } -import silk.option { Option } +import silk.option { Option, none, some } import silk.order { Order } import silk.raw_buffer as RawBuffer import silk.slot as Slot @@ -505,7 +505,7 @@ fn takeSlot(buffer: RawBuffer, index: usize) -> Taken { /// A present result transfers ownership of the removed element. Capacity does not change. pub fn pop(self: &mut Vector) -> Option { if self.length == usize.ZERO { - return Option.none() + return none() } let storage = Intrinsic.replace(self.storage, Empty { anchor: [] }) let taken = match move storage { @@ -521,7 +521,7 @@ pub fn pop(self: &mut Vector) -> Option { // Commits the storage back into the vector and yields the removed element as a present optional. fn finish(self: &mut Vector, storage: Full, value: T) -> Option { self.storage = move storage - return Option.some(move value) + return some(move value) } /// Removes the element at one index, shifting the later elements down. Traps out of range. @@ -883,8 +883,8 @@ pub fn binarySearch(self: &Vector, target: T) -> Option { } if low < self.length { if !((&target) < (&values[low])) { - return Option.some(low) + return some(low) } } - return Option.none() + return none() } diff --git a/packages/compiler/test/IntegerScalars.test.ts b/packages/compiler/test/IntegerScalars.test.ts index f3c816ecf..5cca62340 100644 --- a/packages/compiler/test/IntegerScalars.test.ts +++ b/packages/compiler/test/IntegerScalars.test.ts @@ -7,7 +7,7 @@ import * as Scalar from '../src/Scalar.js' const source = `import silk.i16 as i16 import silk.u8 as u8 -import silk.option { Option, Some, None } +import silk.option { Option } fn overflow() -> Option { return u8.checkedAdd(255, 1) @@ -24,16 +24,16 @@ fn section() -> Option { pub fn main() -> i32 { let failed = match move overflow() { - None {} => 40 - Some { value } => u8.toI32(value) + Option.None => 40 + Option.Some { value } => u8.toI32(value) } let converted = match move convert() { - None {} => 0 - Some { value } => u8.toI32(value) + Option.None => 0 + Option.Some { value } => u8.toI32(value) } let sectioned = match move section() { - None {} => 0 - Some { value } => u8.toI32(value) + Option.None => 0 + Option.Some { value } => u8.toI32(value) } return failed + converted + sectioned - 295 }` @@ -73,14 +73,75 @@ it.effect('lowers checked integer outcomes through LLVM and direct Wasm', () => }), ) +const customCheckedCarrier = `import silk.u8 as u8 + +union Checked { + Present { value: T }, + Absent +} + +fn present(value: T) -> Checked { + return Checked.Present { value: move value } +} + +fn absent() -> Checked { + return Checked.Absent +} + +fn add(left: u8, right: u8) -> Checked { + return Intrinsic.u8CheckedAdd>(left, right, present, absent) +} + +fn value(self: Checked) -> i32 { + return match move self { + Checked.Present { value } => u8.toI32(value) + Checked.Absent => 0 + } +} + +pub fn main() -> i32 { + return value(add(40, 2)) + value(add(255, 1)) +}` + +it.effect('lets checked scalar intrinsics choose a generic nominal carrier', () => + Effect.gen(function* () { + const wasm = yield* Analysis.ofSourceRealized( + 'integer/custom-checked-carrier', + new TextEncoder().encode(customCheckedCarrier), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(wasm), []) + + const evaluated = Analysis.evaluate(wasm) + assert.strictEqual( + evaluated._tag, + 'Completed', + JSON.stringify(evaluated, (_, value) => (typeof value === 'bigint' ? `${value}n` : value), 2), + ) + if (evaluated._tag === 'Completed') assert.strictEqual(evaluated.result.value, 42n) + + const artifact = yield* Analysis.codegenWasm(wasm, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(artifact.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + + const native = yield* Analysis.ofSourceRealized( + 'integer/custom-checked-carrier-native', + new TextEncoder().encode(customCheckedCarrier), + 'aarch64-apple-darwin', + ) + assert.deepEqual(Analysis.diagnostics(native), []) + assert.isAbove((yield* Analysis.codegen(native, { mode: 'release' })).bitcode.length, 0) + }), +) + const characters = `import silk.u32 as u32 import silk.char { fromU32, toU32 } -import silk.option { Some, None } +import silk.option { Option } fn value(input: u32) -> u32 { return match move fromU32(input) { - Some { value } => toU32(value) - None {} => u32.toU32(0) + Option.Some { value } => toU32(value) + Option.None => u32.toU32(0) } } @@ -211,12 +272,11 @@ const integerCase = ( if (operation.result === 'Boolean') return `fn integerCase${ordinal}() -> i32 { if ${invocation} { return 42 } return 0 }` if (operation.result === 'OptionSelf' || operation.result === 'OptionTarget') - return `import silk.option { None } -import silk.option { Some } + return `import silk.option { Option } fn integerCase${ordinal}() -> i32 { return match move ${invocation} { - None {} => 0 - Some<${target.spelling}> { value } => ${target.spelling === 'i32' ? 'value' : `${target.spelling}.toI32(value)`} + Option<${target.spelling}>.None => 0 + Option<${target.spelling}>.Some { value } => ${target.spelling === 'i32' ? 'value' : `${target.spelling}.toI32(value)`} } }` return `fn integerCase${ordinal}() -> i32 { return ${target.spelling === 'i32' ? invocation : `${target.spelling}.toI32(${invocation})`} }` @@ -238,7 +298,7 @@ const matrixSource = (() => { return `${imports} import silk.f32 as f32 import silk.f64 as f64 -import silk.option { Some, None } +import silk.option { Option } ${declarations.join('\n')} fn verify(value: i32) -> () { if value != 42 { let boom = 1 / 0 } } pub fn main() -> i32 { From 937289215fafc8d793d831deed83a5d4ecbb5007 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 15:18:57 -0300 Subject: [PATCH 12/42] test: migrate option and result fixtures --- .../test/BoundOperationWitness.test.ts | 24 +++---- packages/compiler/test/ChildProcess.test.ts | 41 ++++++------ .../test/CompilerArtifactArchitecture.test.ts | 8 +-- .../test/ConditionalConformance.test.ts | 8 +-- .../compiler/test/ExecutionPackage.test.ts | 8 +-- .../test/FileSystemAcceptance.test.ts | 22 +++---- .../test/HashedCollectionDeterminism.test.ts | 2 +- .../test/HashedCollectionOwnership.test.ts | 10 +-- .../test/HashedCollectionPrivilege.test.ts | 2 +- .../compiler/test/HashedCollections.test.ts | 8 +-- packages/compiler/test/HostInput.test.ts | 20 +++--- .../compiler/test/IntrinsicCatalog.test.ts | 8 +-- .../compiler/test/MirNormalization.test.ts | 8 +-- packages/compiler/test/NumberText.test.ts | 14 ++--- .../compiler/test/NumericConstants.test.ts | 8 +-- .../test/OptionResultCombinators.test.ts | 41 ++++++------ packages/compiler/test/OsFileSystem.test.ts | 24 +++---- packages/compiler/test/ResultStdlib.test.ts | 62 +++++++------------ packages/compiler/test/SchedulerFiber.test.ts | 30 ++++----- packages/compiler/test/SelectiveCatch.test.ts | 8 +-- .../test/StdlibNamespaceAcceptance.test.ts | 30 ++++----- .../test/StdlibTypedCountAcceptance.test.ts | 6 +- .../test/StoredCallableDiagnostic.test.ts | 2 +- .../compiler/test/StringAcceptance.test.ts | 14 ++--- .../compiler/test/StringOwnership.test.ts | 8 +-- packages/compiler/test/StringStdlib.test.ts | 14 ++--- .../test/TargetDependentConstants.test.ts | 10 +-- .../test/TemporaryDirectoryAcceptance.test.ts | 8 +-- .../UnicodeNormalizationConformance.test.ts | 20 +++--- packages/compiler/test/UserServices.test.ts | 8 +-- .../compiler/test/VectorAcceptance.test.ts | 21 +++---- packages/compiler/test/VectorSort.test.ts | 16 ++--- .../fixtures/scheduler-fiber/fork-child.silk | 7 +-- packages/compiler/test/support/corpus.ts | 16 +++-- .../test/support/ownedAllocatorSuspension.ts | 16 ++--- 35 files changed, 236 insertions(+), 316 deletions(-) diff --git a/packages/compiler/test/BoundOperationWitness.test.ts b/packages/compiler/test/BoundOperationWitness.test.ts index a06f9083e..9df8669c2 100644 --- a/packages/compiler/test/BoundOperationWitness.test.ts +++ b/packages/compiler/test/BoundOperationWitness.test.ts @@ -249,7 +249,7 @@ it.effect( const outcome = yield* twoEngineValue( 'bound-operation-witness/fallible-weaker-access', `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } pub struct Problem { code: i32 } @@ -271,10 +271,8 @@ fn pending(value: &mut T) -> Effect { fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => error.code - } + Result.Success { value } => value + Result.Failure { error } => error.code } } @@ -366,7 +364,7 @@ it.effect('widens a pure source witness to the exact interface Effect contract', Effect.gen(function* () { const module = 'bound-operation-witness/pure-effect-boundary' const source = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } pub struct Problem {} @@ -386,10 +384,8 @@ fn pending(value: &T) -> Effect { fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => 0 - } + Result.Success { value } => value + Result.Failure { error } => 0 } } @@ -495,7 +491,7 @@ it.effect('widens a smaller Effect witness row at the interface boundary', () => const value = yield* evaluatedValue( 'bound-operation-witness/smaller-effect-row', `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } pub struct Problem {} pub struct Extra {} @@ -516,13 +512,11 @@ fn pending(value: &T) -> Effect { fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { + Result.Success { value } => value + Result.Failure { error } => match move error { Problem {} => 0 Extra {} => 0 } - } } } diff --git a/packages/compiler/test/ChildProcess.test.ts b/packages/compiler/test/ChildProcess.test.ts index 8688c41f8..f958027b4 100644 --- a/packages/compiler/test/ChildProcess.test.ts +++ b/packages/compiler/test/ChildProcess.test.ts @@ -102,8 +102,8 @@ import silk.child_process { terminatingSignal } import silk.filesystem { FileError, fromBytes as pathFromBytes } -import silk.option { None, Option, Some } -import silk.result { Failure, Result, Success } +import silk.option { Option } +import silk.result { Result } ` @@ -111,24 +111,21 @@ import silk.result { Failure, Result, Success } const recovery = `import silk.child_process { ProcessError } import silk.allocator { OutOfMemoryError } import silk.filesystem { FileError } -import silk.option { None } -import silk.option { Some } +import silk.option { Option } import silk.result { Result } pub fn main() -> i32 { let attempted = run Intrinsic.effectResult(program()) return match move attempted { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { + Result.Success { value } => value + Result.Failure { error } => match move error { ProcessError processFailure => match move providerCode(&processFailure) { - Some { value } => + Option.Some { value } => 70 + processFailure.reason.code + 10 * processFailure.operation.code + value - None {} => 70 + processFailure.reason.code + 10 * processFailure.operation.code + _ => 70 + processFailure.reason.code + 10 * processFailure.operation.code } OutOfMemoryError exhausted => 98 FileError invalid => 99 } - } } }` @@ -191,8 +188,8 @@ it.effect('carries an exit code, captured output, and captured errors as one own if outputBytes(&outcome)[usize.ZERO] != u8.toU8(115) { return 3 } if errorBytes(&outcome).length != usize.ZERO { return 4 } return match move exitCode(&outcome) { - Some { value } => 42 + value - None {} => 5 + Option.Some { value } => 42 + value + Option.None => 5 }`, ), ) @@ -217,8 +214,8 @@ it.effect('reports a nonzero exit code as outcome data rather than as a typed fa ) if isSignaled(&outcome) { return 1 } return match move exitCode(&outcome) { - Some { value } => 39 + value - None {} => 2 + Option.Some { value } => 39 + value + Option.None => 2 }`, ), ) @@ -243,13 +240,13 @@ it.effect('separates termination by a signal from an ordinary exit code', () => if isSignaled(&outcome) == false { return 1 } // A signal never presents itself as an exit code, so no caller reads 9 as a return value. let absent = match move exitCode(&outcome) { - Some { value } => false - None {} => true + Option.Some { value } => false + Option.None => true } if absent == false { return 2 } return match move terminatingSignal(&outcome) { - Some { value } => 33 + value - None {} => 3 + Option.Some { value } => 33 + value + Option.None => 3 }`, ), ) @@ -326,8 +323,8 @@ const nativeEcho = if errorBytes(&outcome).length != usize.add(0, 2) { return 5 } if errorBytes(&outcome)[usize.ZERO] != u8.toU8(101) { return 6 } return match move exitCode(&outcome) { - Some { value } => 42 + value - None {} => 7 + Option.Some { value } => 42 + value + Option.None => 7 }`) it.effect('runs a program that exits zero and owns everything it captured', () => @@ -364,8 +361,8 @@ it.effect('keeps a nonzero exit code on the success channel through the native p if outputBytes(&outcome).length != usize.ONE { return 2 } if errorBytes(&outcome).length != usize.add(0, 4) { return 3 } return match move exitCode(&outcome) { - Some { value } => 39 + value - None {} => 4 + Option.Some { value } => 39 + value + Option.None => 4 }`), ) assert.deepEqual(Analysis.diagnostics(self), []) diff --git a/packages/compiler/test/CompilerArtifactArchitecture.test.ts b/packages/compiler/test/CompilerArtifactArchitecture.test.ts index 3971dfd4d..957129ada 100644 --- a/packages/compiler/test/CompilerArtifactArchitecture.test.ts +++ b/packages/compiler/test/CompilerArtifactArchitecture.test.ts @@ -58,7 +58,7 @@ it.effect( it.effect('keeps synchronous Effect core artifacts free of concurrency runtime ABI', () => Effect.gen(function* () { const source = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } service Clock { effect fn value() -> i32 ? &Clock } struct FixedClock { value: i32 } effect fn clockValue(self: &FixedClock) -> i32 { return self.value } @@ -69,10 +69,8 @@ pub fn main() -> i32 { let closed = Intrinsic.bindRequirement(read(), &clock) let completed = run Effect.result(closed) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: answer } => answer - Failure { error: impossible } => 0 - } + Result.Success { value: answer } => answer + Result.Failure { error: impossible } => 0 } }` const snapshot = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/ConditionalConformance.test.ts b/packages/compiler/test/ConditionalConformance.test.ts index cc0234f8a..7a43e7ae6 100644 --- a/packages/compiler/test/ConditionalConformance.test.ts +++ b/packages/compiler/test/ConditionalConformance.test.ts @@ -793,7 +793,7 @@ it.effect('infers operand binders and propagates a failing smaller generic witne const snapshot = yield* analyze( module, `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct Problem { code: i32 } struct Extra {} @@ -816,13 +816,11 @@ fn pending(value: &T) -> Effect { fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { + Result.Success { value } => value + Result.Failure { error } => match move error { Problem { code } => code Extra {} => 0 } - } } } diff --git a/packages/compiler/test/ExecutionPackage.test.ts b/packages/compiler/test/ExecutionPackage.test.ts index 1a50a53c4..c1b0e574b 100644 --- a/packages/compiler/test/ExecutionPackage.test.ts +++ b/packages/compiler/test/ExecutionPackage.test.ts @@ -677,17 +677,15 @@ it.effect('completes a reified typed failure as data and releases its package on import silk.allocator { Allocator } import silk.effect as Effect import silk.execution as Execution -import silk.result { Result, Success, Failure } +import silk.result { Result } struct State { value: i32 } struct Failed { code: i32 } struct Ready {} fn ready(state: &Ready) -> () { return () } fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { Failed { code } => code } - } + Result.Success { value } => value + Result.Failure { error } => match move error { Failed { code } => code } } } fn complete(state: &mut State, value: Result) -> () { diff --git a/packages/compiler/test/FileSystemAcceptance.test.ts b/packages/compiler/test/FileSystemAcceptance.test.ts index c2f42c058..eb5041622 100644 --- a/packages/compiler/test/FileSystemAcceptance.test.ts +++ b/packages/compiler/test/FileSystemAcceptance.test.ts @@ -51,7 +51,7 @@ import silk.filesystem { unsupported } import silk.bytes { Bytes, asSlice as bytesSlice, copy as bytesCopy } -import silk.result { Failure, Result, Success } +import silk.result { Result } import silk.vector { Vector, append as vectorAppend, @@ -240,10 +240,8 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { pub fn main() -> i32 { let completed = run Intrinsic.effectResult(program()) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => 10 - } + Result.Success { value } => value + Result.Failure { error } => 10 } }` @@ -255,12 +253,12 @@ import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.filesystem { FileError } import silk.filesystem { Path, isRoot, make, name, parent, resolve, root, view } -import silk.option { None, Option, Some } -import silk.result { Failure, Result, Success } +import silk.option { Option } +import silk.result { Result } fn matchesParent(possible: Option, expected: string) -> bool { return match move possible { - None {} => false - Some { value } => view(&value) == expected + Option.None => false + Option.Some { value } => view(&value) == expected } } @@ -284,10 +282,8 @@ effect fn check() -> i32 ! FileError | OutOfMemoryError { pub fn main() -> i32 { let completed = run Intrinsic.effectResult(check()) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => 2 - } + Result.Success { value } => value + Result.Failure { error } => 2 } }` const snapshot = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/HashedCollectionDeterminism.test.ts b/packages/compiler/test/HashedCollectionDeterminism.test.ts index 5282d185e..f886cf088 100644 --- a/packages/compiler/test/HashedCollectionDeterminism.test.ts +++ b/packages/compiler/test/HashedCollectionDeterminism.test.ts @@ -96,7 +96,7 @@ const mapImports = `import silk.hash as Hash import silk.hash { HashKey, HashSeed, Word } import silk.i32 as i32 import silk.hash_map { HashMap, bucketCount, insert, keyAt, length, make, occupiedAt } -import silk.option { Option, Some, None } +import silk.option { Option } import silk.u64 as u64 import silk.usize as usize` diff --git a/packages/compiler/test/HashedCollectionOwnership.test.ts b/packages/compiler/test/HashedCollectionOwnership.test.ts index 39ba7c0f8..0138bbbc7 100644 --- a/packages/compiler/test/HashedCollectionOwnership.test.ts +++ b/packages/compiler/test/HashedCollectionOwnership.test.ts @@ -54,7 +54,7 @@ import silk.layout { Layout } import silk.hash as Hash import silk.hash { HashKey, HashSeed } import silk.hash_map { HashMap, contains, insert, length, make, remove, withMut } -import silk.option { Option, Some, None } +import silk.option { Option } struct Handle { tag: i32 @@ -319,8 +319,8 @@ it.effect('releases the replaced value and the replaced key when an overwrite la |> Effect.provideMut(&mut allocator) if length(&map) != 1 { return 1 } let replaced = match move displaced { - Some { value } => tagOf(move value) - None {} => 0 + Option.Some { value } => tagOf(move value) + Option.None => 0 } if replaced != 11 { return 2 } return 42`), @@ -352,8 +352,8 @@ it.effect('transfers a removed value out and releases the key the map held', () let taken = remove(&mut map, move probe) if length(&map) != 1 { return 1 } let carried = match move taken { - Some { value } => tagOf(move value) - None {} => 0 + Option.Some { value } => tagOf(move value) + Option.None => 0 } // The removed value is the caller's now, and the map still owns the entry it kept. if carried != 20 { return 2 } diff --git a/packages/compiler/test/HashedCollectionPrivilege.test.ts b/packages/compiler/test/HashedCollectionPrivilege.test.ts index 2d7a553b9..565045da1 100644 --- a/packages/compiler/test/HashedCollectionPrivilege.test.ts +++ b/packages/compiler/test/HashedCollectionPrivilege.test.ts @@ -35,7 +35,7 @@ import silk.i32 as i32 import silk.hash as Hash import silk.hash { HashKey, HashSeed, Word } import silk.hash_map { HashMap, contains, get, insert, length, make, remove } -import silk.option { Option, Some, None } +import silk.option { Option } effect fn build() -> i32 ! OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() diff --git a/packages/compiler/test/HashedCollections.test.ts b/packages/compiler/test/HashedCollections.test.ts index 0815064dd..8df410b0e 100644 --- a/packages/compiler/test/HashedCollections.test.ts +++ b/packages/compiler/test/HashedCollections.test.ts @@ -75,7 +75,7 @@ import silk.hash_map { valueAt, withMut } -import silk.option { Option, Some, None } +import silk.option { Option } import silk.u64 as u64 import silk.usize as usize` @@ -372,7 +372,7 @@ import silk.hash_set { occupiedAt, remove } -import silk.option { Option, Some, None }` +import silk.option { Option }` it.effect( 'refuses a second equivalent element, and answers membership before and after removal', @@ -394,8 +394,8 @@ it.effect( if !contains(&seen, Hash.word(9)) { return 5 } let taken = remove(&mut seen, Hash.word(9)) let gone = match move taken { - Some { value } => u64.toI32(value.value) - None {} => 0 + Option.Some { value } => u64.toI32(value.value) + Option.None => 0 } if gone != 9 { return 6 } if contains(&seen, Hash.word(9)) { return 7 } diff --git a/packages/compiler/test/HostInput.test.ts b/packages/compiler/test/HostInput.test.ts index 3fef53ece..33b8b896b 100644 --- a/packages/compiler/test/HostInput.test.ts +++ b/packages/compiler/test/HostInput.test.ts @@ -114,9 +114,7 @@ impl HostInput for Broken { /** Shared readers. Owned host bytes are read through a stable binding, never through an index. */ const support = `import silk.bytes { Bytes } import silk.host_input { HostInputError } -import silk.option { None } import silk.option { Option } -import silk.option { Some } import silk.result { Result } import silk.string { InvalidUtf8 } import silk.usize as usize @@ -130,17 +128,15 @@ fn byteAt(entry: &Bytes, index: usize) -> u8 { fn present(value: Option) -> bool { return match move value { - Some { value: bytes } => true - None {} => false + Option.Some { value: bytes } => true + Option.None => false } } fn decodes(entry: &Bytes) -> bool { return match move text(bytesSlice(entry)) { - Result { value: outcome } => match move outcome { - Success { value: view } => true - Failure { error: invalid } => false - } + Result.Success { value: view } => true + Result.Failure { error: invalid } => false } } @@ -150,8 +146,8 @@ effect fn raiseMissing() -> never ! HostInputError { effect fn required(found: Option) -> Bytes ! HostInputError { return match move found { - Some { value: bytes } => move bytes - None {} => run raiseMissing() + Option.Some { value: bytes } => move bytes + Option.None => run raiseMissing() } } ` @@ -169,8 +165,8 @@ import silk.host_input { variableNamed, workingDirectory } -import silk.option { None, Option, Some, none, some } -import silk.result { Failure, Result, Success } +import silk.option { Option, none, some } +import silk.result { Result } import silk.string { InvalidUtf8 } import silk.vector { Vector, length as vectorLength, remove as vectorRemove } diff --git a/packages/compiler/test/IntrinsicCatalog.test.ts b/packages/compiler/test/IntrinsicCatalog.test.ts index 18120817f..9740df852 100644 --- a/packages/compiler/test/IntrinsicCatalog.test.ts +++ b/packages/compiler/test/IntrinsicCatalog.test.ts @@ -203,16 +203,14 @@ pub fn main() -> i32 { return false }`, `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct ResultProblem {} effect fn succeed() -> i32 ! ResultProblem { return 42 } pub effect fn main() -> i32 { let completed = run Effect.result(succeed()) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => 0 - } + Result.Success { value } => value + Result.Failure { error } => 0 } }`, `struct CatalogProblem {} diff --git a/packages/compiler/test/MirNormalization.test.ts b/packages/compiler/test/MirNormalization.test.ts index a500839a5..70ea5f503 100644 --- a/packages/compiler/test/MirNormalization.test.ts +++ b/packages/compiler/test/MirNormalization.test.ts @@ -10,16 +10,14 @@ import * as Projections from './support/projections.js' const encoder = new TextEncoder() const source = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } effect fn succeed(value: i32) -> i32 { return value } fn addOne(value: i32) -> i32 { return value + 1 } effect fn userMap(self: once Effect, onSuccess: once fn(i32) -> i32) -> i32 { let completed = run Effect.result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => onSuccess(move value) - Failure { error } => move error - } + Result.Success { value } => onSuccess(move value) + Result.Failure { error } => move error } } pub fn main() -> i32 { return run succeed(41) |> userMap(addOne) }` diff --git a/packages/compiler/test/NumberText.test.ts b/packages/compiler/test/NumberText.test.ts index 3e62c3682..4e1a28938 100644 --- a/packages/compiler/test/NumberText.test.ts +++ b/packages/compiler/test/NumberText.test.ts @@ -53,7 +53,7 @@ const prelude = `${Scalar.integers() .join('\n')} import silk.string { String, copy, append, appendOwned, ownedUtf8Bytes, utf8Bytes } import silk.format { ParseError, NotANumber, OutOfRange } -import silk.result { Result, Success, Failure } +import silk.result { Result } fn sameText(text: &String, expected: string) -> bool { let actual = ownedUtf8Bytes(text) @@ -123,10 +123,8 @@ const valueCheck = (name: string, spelling: string, text: string, expected: stri import silk.result { Result } fn ${name}() -> i32 { return match move ${spelling}.parse("${text}") { - Result<${spelling}, ParseError> { value: outcome } => match move outcome { - Success<${spelling}> { value } => ${isSigned(spelling) ? 'sameSigned' : 'sameUnsigned'}(${spelling}.${widen(spelling)}(value), ${expected}) - Failure { error } => 1 - } + Result<${spelling}, ParseError>.Success { value } => ${isSigned(spelling) ? 'sameSigned' : 'sameUnsigned'}(${spelling}.${widen(spelling)}(value), ${expected}) + Result<${spelling}, ParseError>.Failure { error } => 1 } }`, }) @@ -156,10 +154,8 @@ import silk.result { Result } import silk.usize as usize fn ${name}() -> i32 { return match move ${spelling}.parse("${text}") { - Result<${spelling}, ParseError> { value: outcome } => match move outcome { - Success<${spelling}> { value } => 1 - Failure { error } => ${failure} - } + Result<${spelling}, ParseError>.Success { value } => 1 + Result<${spelling}, ParseError>.Failure { error } => ${failure} } }`, } diff --git a/packages/compiler/test/NumericConstants.test.ts b/packages/compiler/test/NumericConstants.test.ts index 1d00cffe7..b9da8b757 100644 --- a/packages/compiler/test/NumericConstants.test.ts +++ b/packages/compiler/test/NumericConstants.test.ts @@ -167,15 +167,15 @@ it.effect('reports no invalid-constant diagnostic for any stdlib declaration', ( const checkedCases = integerSpellings.flatMap((spelling, ordinal) => [ { name: `pastMax${ordinal}`, - body: `match move ${spelling}.checkedAdd(${spelling}.MAX, ${typedOne(spelling)}) { Some<${spelling}> { value: result } => 0 None nothing => 42 }`, + body: `match move ${spelling}.checkedAdd(${spelling}.MAX, ${typedOne(spelling)}) { Option<${spelling}>.Some { value: result } => 0 _ => 42 }`, }, { name: `belowMin${ordinal}`, - body: `match move ${spelling}.checkedSubtract(${spelling}.MIN, ${typedOne(spelling)}) { Some<${spelling}> { value: result } => 0 None nothing => 42 }`, + body: `match move ${spelling}.checkedSubtract(${spelling}.MIN, ${typedOne(spelling)}) { Option<${spelling}>.Some { value: result } => 0 _ => 42 }`, }, { name: `insideBound${ordinal}`, - body: `match move ${spelling}.checkedAdd(${spelling}.MIN, ${typedOne(spelling)}) { Some<${spelling}> { value: result } => 42 None nothing => 0 }`, + body: `match move ${spelling}.checkedAdd(${spelling}.MIN, ${typedOne(spelling)}) { Option<${spelling}>.Some { value: result } => 42 _ => 0 }`, }, ]) @@ -198,7 +198,7 @@ const probeNames = acceptanceCases.map((declared) => declared.slice(3, declared. const acceptance = `${[...integerSpellings, ...floatSpellings] .map((spelling) => `import silk.${spelling} as ${spelling}`) .join('\n')} -import silk.option { Some, None } +import silk.option { Option } ${acceptanceCases.join('\n')} diff --git a/packages/compiler/test/OptionResultCombinators.test.ts b/packages/compiler/test/OptionResultCombinators.test.ts index 3fbbce012..144b7d86c 100644 --- a/packages/compiler/test/OptionResultCombinators.test.ts +++ b/packages/compiler/test/OptionResultCombinators.test.ts @@ -13,7 +13,7 @@ const ascii = (value: string): Uint8Array => * transform runs on exactly one of the two arms. `unwrapOr` then answers with the fallback only * for the absent option. */ -const optionMap = `import silk.option { Option } +const optionMap = `import silk.option as Option fn double(value: i32) -> i32 { return value * 2 } pub fn main() -> i32 { @@ -29,14 +29,14 @@ pub fn main() -> i32 { * `flatMap` keeps the outcome one Option deep. `oneLayer` names `Option` exactly, so a * combinator that nested its result into `Option>` would not typecheck here. */ -const optionFlatMap = `import silk.option { Option } +const optionFlatMap = `import silk.option as Option -fn halve(value: i32) -> Option { +fn halve(value: i32) -> Option.Option { if value == 0 { return Option.none() } return Option.some(value / 2) } -fn oneLayer(self: Option) -> Option { return move self } +fn oneLayer(self: Option.Option) -> Option.Option { return move self } pub fn main() -> i32 { let present = Option.some(80) @@ -50,7 +50,7 @@ pub fn main() -> i32 { * `mapError` rewrites a failure value and carries a success through untouched, including across a * change of failure type: `widen` never runs on the success arm. */ -const resultMapError = `import silk.result { Result, Success, Failure } +const resultMapError = `import silk.result as Result struct Wide { code: i32 } @@ -58,12 +58,10 @@ fn widen(error: i32) -> Wide { return Wide { code: error + 30 } } -fn observe(self: Result) -> i32 { +fn observe(self: Result.Result) -> i32 { return match move self { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => error.code - } + Result.Result.Success { value } => value + Result.Result.Failure { error } => error.code } } @@ -76,14 +74,14 @@ pub fn main() -> i32 { }` /** - * `map`, `flatMap`, and `unwrapOr` over both channels, with `isSuccess` and `isFailure` testing an - * outcome through a shared borrow that leaves it usable afterwards. + * `map`, `flatMap`, and `unwrapOr` over both channels, with borrowed matches testing an outcome + * while leaving it usable afterwards. */ -const resultCombinators = `import silk.result { Result } +const resultCombinators = `import silk.result as Result fn addTwo(value: i32) -> i32 { return value + 2 } -fn halve(value: i32) -> Result { +fn halve(value: i32) -> Result.Result { if value == 0 { return Result.failResult(9) } return Result.succeed(value / 2) } @@ -91,12 +89,19 @@ fn halve(value: i32) -> Result { pub fn main() -> i32 { let succeeded = Result.succeed(36) let mapped = Result.map(move succeeded, addTwo) - if Result.isSuccess(&mapped) {} else { return 1 } - if Result.isFailure(&mapped) { return 2 } + let successCheck = match &mapped { + Result.Result.Success { value } => true + Result.Result.Failure { error } => false + } + if successCheck {} else { return 1 } let chained = Result.flatMap(move mapped, halve) let failed = Result.failResult(7) let mappedFailure = Result.map(move failed, addTwo) - if Result.isFailure(&mappedFailure) {} else { return 3 } + let failureCheck = match &mappedFailure { + Result.Result.Success { value } => false + Result.Result.Failure { error } => true + } + if failureCheck {} else { return 3 } return Result.unwrapOr(move chained, 0) + Result.unwrapOr(move mappedFailure, 23) }` @@ -113,7 +118,7 @@ import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.layout { Layout } -import silk.option { Option } +import silk.option as Option struct Token { storage: Allocation } diff --git a/packages/compiler/test/OsFileSystem.test.ts b/packages/compiler/test/OsFileSystem.test.ts index f9025b8b1..93a90563e 100644 --- a/packages/compiler/test/OsFileSystem.test.ts +++ b/packages/compiler/test/OsFileSystem.test.ts @@ -176,7 +176,7 @@ import silk.usize as usize import silk.os_filesystem { make as osMake } import silk.bytes { asSlice as bytesSlice } import silk.filesystem { FileError, FileSystem, make as pathMake } -import silk.result { Failure, Result, Success } +import silk.result { Result } effect fn program() -> i32 ! FileError | OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() @@ -195,13 +195,11 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { pub fn main() -> i32 { let attempted = run Intrinsic.effectResult(program()) return match move attempted { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { + Result.Success { value } => value + Result.Failure { error } => match move error { FileError failure => failure.reason.code OutOfMemoryError exhausted => 99 } - } } }` const snapshot = yield* Analysis.ofSourceRealized( @@ -288,7 +286,7 @@ import silk.effect as Effect import silk.usize as usize import silk.os_filesystem { make as osMake } import silk.filesystem { DirectoryEntry, FileError, FileSystem, root as pathRoot, view as pathView } -import silk.result { Failure, Result, Success } +import silk.result { Result } import silk.vector { asSlice as vectorSlice } fn pathMatches(entries: &[DirectoryEntry], index: usize, expected: string) -> bool { @@ -315,10 +313,8 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { pub fn main() -> i32 { let attempted = run Intrinsic.effectResult(program()) return match move attempted { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => 10 - } + Result.Success { value } => value + Result.Failure { error } => 10 } }` const snapshot = yield* Analysis.ofSourceRealized( @@ -624,7 +620,7 @@ import silk.usize as usize import silk.os_filesystem { make as osMake } import silk.bytes { asSlice as bytesSlice } import silk.filesystem { FileError, FileSystem, make as pathMake } -import silk.result { Failure, Result, Success } +import silk.result { Result } effect fn program() -> i32 ! FileError | OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() @@ -665,10 +661,8 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { pub fn main() -> i32 { let completed = run Intrinsic.effectResult(program()) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => 10 - } + Result.Success { value } => value + Result.Failure { error } => 10 } }` const compiled = yield* Driver.compile({ diff --git a/packages/compiler/test/ResultStdlib.test.ts b/packages/compiler/test/ResultStdlib.test.ts index 6b1458767..ea22ce6a6 100644 --- a/packages/compiler/test/ResultStdlib.test.ts +++ b/packages/compiler/test/ResultStdlib.test.ts @@ -5,14 +5,12 @@ import * as Analysis from '../src/Analysis.js' const ascii = (value: string): Uint8Array => Uint8Array.from(value, (character) => character.charCodeAt(0)) -const copyPayloads = `import silk.result { Result, Success, Failure, succeed, failResult } +const copyPayloads = `import silk.result { Result, succeed, failResult } fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value: successValue } => successValue - Failure { error: failureValue } => failureValue - } + Result.Success { value: successValue } => successValue + Result.Failure { error: failureValue } => failureValue } } @@ -28,7 +26,7 @@ import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.layout { Layout } -import silk.result { Result, Success, Failure, succeed } +import silk.result { Result, succeed } struct Token { storage: Allocation } @@ -45,10 +43,8 @@ fn consume(token: Token) -> i32 { fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value: token } => consume(move token) - Failure { error: failureValue } => failureValue - } + Result.Success { value: token } => consume(move token) + Result.Failure { error: failureValue } => failureValue } } @@ -66,7 +62,7 @@ effect fn recover(error: OutOfMemoryError) -> i32 { return 0 } pub fn main() -> i32 { return run Effect.catchAll(build(), recover) }` const reifiedEffect = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct First { code: i32 } struct Second { code: i32 } @@ -79,13 +75,11 @@ effect fn choose(first: bool) -> i32 ! First | Second { effect fn inspect(first: bool) -> i32 { let completed = run Effect.result(choose(first)) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: successValue } => successValue - Failure { error } => match move error { + Result.Success { value: successValue } => successValue + Result.Failure { error } => match move error { First { code: firstCode } => firstCode Second { code: secondCode } => secondCode } - } } } @@ -101,7 +95,7 @@ import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.layout { Layout } -import silk.result { Result, Success, Failure } +import silk.result { Result } effect fn allocateOne() -> Allocation ! OutOfMemoryError ? &mut Allocator { let layout = Layout.of() @@ -116,10 +110,8 @@ effect fn build() -> i32 { let mut allocator = Allocator.systemAllocatorProvider() let completed = run Intrinsic.bindRequirementMut(attempt(), &mut allocator) return match move completed { - Result { value: outcome } => match move outcome { - Success { value: storage } => release(move storage) - Failure { error: ignored } => 0 - } + Result.Success { value: storage } => release(move storage) + Result.Failure { error: ignored } => 0 } } @@ -163,7 +155,7 @@ pub fn main() -> i32 { }` const sourceDefinedMaps = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct First { code: i32 } struct Second { code: i32 } @@ -175,10 +167,8 @@ fn toSecond(error: First) -> Second { return Second { code: error.code + 40 } } fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value: answer } => answer - Failure { error } => error.code - } + Result.Success { value: answer } => answer + Result.Failure { error } => error.code } } @@ -189,7 +179,7 @@ pub fn main() -> i32 { }` const sourceDefinedEffectfulCombinators = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct First { code: i32 } struct Second { code: i32 } @@ -202,22 +192,18 @@ effect fn recover(error: First) -> i32 ! Second { return error.code + 40 } fn observeBoth(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { + Result.Success { value } => value + Result.Failure { error } => match move error { First { code } => code Second { code } => code } - } } } fn observeSecond(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => error.code - } + Result.Success { value } => value + Result.Failure { error } => error.code } } @@ -229,7 +215,7 @@ pub fn main() -> i32 { }` const sourceDefinedRetry = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct Problem { code: i32 } @@ -238,10 +224,8 @@ effect fn failAlways() -> i32 ! Problem { fail Problem { code: 2 } } fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => error.code - } + Result.Success { value } => value + Result.Failure { error } => error.code } } diff --git a/packages/compiler/test/SchedulerFiber.test.ts b/packages/compiler/test/SchedulerFiber.test.ts index 933e36b4e..7d6d7d3d6 100644 --- a/packages/compiler/test/SchedulerFiber.test.ts +++ b/packages/compiler/test/SchedulerFiber.test.ts @@ -39,10 +39,9 @@ fn verifyTaskIdRefusal( outcome: Result, fresh: Scheduler.TaskId, ) -> i32 { - let Result { value: phase } = move outcome - return match move phase { - Success { value: reserved } => -3 - Failure { error } => verifyFreshTaskId(move error, fresh) + return match move outcome { + Result.Success { value: reserved } => -3 + Result.Failure { error } => verifyFreshTaskId(move error, fresh) } } @@ -83,10 +82,9 @@ fn taskIdBoundaryFailed(error: OutOfMemoryError | Scheduler.TaskIdExhaustedError pub fn main() -> i32 { let outcome = run Effect.result(taskIdBoundary()) - let Result { value: phase } = move outcome - return match move phase { - Success { value: answer } => answer - Failure { error } => + return match move outcome { + Result.Success { value: answer } => answer + Result.Failure { error } => taskIdBoundaryFailed(move error) } }` @@ -193,12 +191,11 @@ fn finishPublicationInsertion( Allocator.OutOfMemoryError >, ) -> () { - let Result.Result< - Option.Option, - Allocator.OutOfMemoryError - > { value } = move outcome - return match move value { - Result.Success> { value: previous } => + return match move outcome { + Result.Result< + Option.Option, + Allocator.OutOfMemoryError + >.Success { value: previous } => finishAcceptedPublicationInsertion( move store, identity, @@ -207,7 +204,10 @@ fn finishPublicationInsertion( move wake, move previous, ) - Result.Failure { error } => + Result.Result< + Option.Option, + Allocator.OutOfMemoryError + >.Failure { error } => finishRefusedPublicationInsertion( move store, move response, diff --git a/packages/compiler/test/SelectiveCatch.test.ts b/packages/compiler/test/SelectiveCatch.test.ts index 567b957d7..c91f583d4 100644 --- a/packages/compiler/test/SelectiveCatch.test.ts +++ b/packages/compiler/test/SelectiveCatch.test.ts @@ -254,17 +254,15 @@ pub fn main() -> i32 { }` const infallibleRunLoanSource = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct Token { value: i32 } effect fn succeed(value: i32) -> i32 { return value } fn add(value: i32, token: &Token) -> i32 { return value + token.value } effect fn userMap(self: once Effect, onSuccess: once fn(i32) -> i32) -> i32 { let completed = run Effect.result(move self) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => onSuccess(move value) - Failure { error } => move error - } + Result.Success { value } => onSuccess(move value) + Result.Failure { error } => move error } } pub fn main() -> i32 { diff --git a/packages/compiler/test/StdlibNamespaceAcceptance.test.ts b/packages/compiler/test/StdlibNamespaceAcceptance.test.ts index 14aa264f3..a9f5c738c 100644 --- a/packages/compiler/test/StdlibNamespaceAcceptance.test.ts +++ b/packages/compiler/test/StdlibNamespaceAcceptance.test.ts @@ -10,19 +10,17 @@ const ascii = (value: string): Uint8Array => * Every manifest namespace is auto-injected into user scope, so a program names Option, Result, * and Vector as qualified actors without writing a single import statement. */ -const qualified = `import silk.option { None } -import silk.option { Option } -import silk.option { Some } -import silk.result { Result } -import silk.vector { Vector } -fn present(value: Option) -> i32 { +const qualified = `import silk.option as Option +import silk.result as Result +import silk.vector as Vector +fn present(value: Option.Option) -> i32 { return match move value { - None {} => 0 - Some { value: carried } => carried + Option.Option.None => 0 + Option.Option.Some { value: carried } => carried } } -fn settled(value: Result) -> i32 { +fn settled(value: Result.Result) -> i32 { drop value return 2 } @@ -35,22 +33,20 @@ pub fn main() -> i32 { /** The selective import form keeps resolving the same members alongside the injected namespaces. */ const selective = `import silk.vector { Vector, make } -import silk.option { Option, Some, None, some } -import silk.result { Result, Success, Failure, succeed } +import silk.option { Option, some } +import silk.result { Result, succeed } fn settled(value: Result) -> i32 { return match move value { - Result { value: outcome } => match move outcome { - Success { value: carried } => carried - Failure { error: failure } => failure - } + Result.Success { value: carried } => carried + Result.Failure { error: failure } => failure } } fn present(value: Option) -> i32 { return match move value { - None {} => 0 - Some { value: carried } => carried + Option.None => 0 + Option.Some { value: carried } => carried } } diff --git a/packages/compiler/test/StdlibTypedCountAcceptance.test.ts b/packages/compiler/test/StdlibTypedCountAcceptance.test.ts index 00771cabc..c7b6e9014 100644 --- a/packages/compiler/test/StdlibTypedCountAcceptance.test.ts +++ b/packages/compiler/test/StdlibTypedCountAcceptance.test.ts @@ -154,13 +154,13 @@ import silk.string { fromUtf8, utf8Bytes } -import silk.option { Some, None } +import silk.option { Option } import silk.char { toU32 as charToU32 } fn scalarSum(value: string, cursor: ScalarCursor) -> u32 { return match move nextScalar(value, move cursor) { - Some { value: step } => continueSum(value, move step) - None nothing => u32.toU32(0) + Option.Some { value: step } => continueSum(value, move step) + Option.None => u32.toU32(0) } } diff --git a/packages/compiler/test/StoredCallableDiagnostic.test.ts b/packages/compiler/test/StoredCallableDiagnostic.test.ts index 09646d17a..c299f1cc5 100644 --- a/packages/compiler/test/StoredCallableDiagnostic.test.ts +++ b/packages/compiler/test/StoredCallableDiagnostic.test.ts @@ -208,7 +208,7 @@ pub fn main() -> i32 { it.effect('points a stdlib construction reached through inference at the user call', () => Effect.gen(function* () { - // `Option.some(i32.add(1))` specializes `Some` with a callable argument. The construction + // `Option.some(i32.add(1))` specializes `Option.Some` with a callable argument. The construction // that cannot receive a layout lives inside silk/option, but the callable was written at the // user's call, so the primary span is the user source and the stdlib construction is related // provenance. diff --git a/packages/compiler/test/StringAcceptance.test.ts b/packages/compiler/test/StringAcceptance.test.ts index 1e6eaab4f..68398caf8 100644 --- a/packages/compiler/test/StringAcceptance.test.ts +++ b/packages/compiler/test/StringAcceptance.test.ts @@ -44,13 +44,13 @@ import silk.string { scalarValue, nextCursor } -import silk.option { Some, None } +import silk.option { Option } import silk.char { toU32 as charToU32 } fn scalarSum(value: string, cursor: ScalarCursor) -> u32 { return match move nextScalar(value, move cursor) { - Some { value: step } => continueSum(value, move step) - None nothing => u32.toU32(0) + Option.Some { value: step } => continueSum(value, move step) + Option.None => u32.toU32(0) } } @@ -89,15 +89,13 @@ it.effect( const validation = `import silk.usize as usize import silk.string { InvalidUtf8, fromUtf8, byteLength } -import silk.result { Result, Success, Failure } +import silk.result { Result } fn inspect(bytes: &[u8]) -> i32 { let result = fromUtf8(bytes) return match move result { - Result { value: outcome } => match move outcome { - Success { value } => usize.toI32(byteLength(value)) - Failure { error } => usize.toI32(error.offset) + 40 - } + Result.Success { value } => usize.toI32(byteLength(value)) + Result.Failure { error } => usize.toI32(error.offset) + 40 } } diff --git a/packages/compiler/test/StringOwnership.test.ts b/packages/compiler/test/StringOwnership.test.ts index 98edb9eee..9bbe11ed2 100644 --- a/packages/compiler/test/StringOwnership.test.ts +++ b/packages/compiler/test/StringOwnership.test.ts @@ -107,7 +107,7 @@ pub fn main() -> i32 { return 0 }`) it.effect('preserves loans nested inside generic result data', () => Effect.gen(function* () { const self = yield* snapshot(`import silk.u8 as u8 -import silk.result { Result, Success, Failure, succeed, failResult } +import silk.result { Result, succeed, failResult } struct InvalidUtf8 { offset: usize } fn validate(bytes: &[u8], accepted: bool) -> Result { if accepted { @@ -120,10 +120,8 @@ fn validate(bytes: &[u8], accepted: bool) -> Result { } fn observe(result: Result) -> usize { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => Intrinsic.stringByteLength(value) - Failure { error } => error.offset - } + Result.Success { value } => Intrinsic.stringByteLength(value) + Result.Failure { error } => error.offset } } fn escape(bytes: &[u8]) -> Result { return validate(bytes, true) } diff --git a/packages/compiler/test/StringStdlib.test.ts b/packages/compiler/test/StringStdlib.test.ts index af011ead1..aebb55f0a 100644 --- a/packages/compiler/test/StringStdlib.test.ts +++ b/packages/compiler/test/StringStdlib.test.ts @@ -20,14 +20,12 @@ const diagnosticSummary = (snapshot: Analysis.Snapshot) => const validation = `import silk.usize as usize import silk.string { InvalidUtf8, fromUtf8, byteLength } -import silk.result { Result, Success, Failure } +import silk.result { Result } fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => usize.toI32(byteLength(value)) - Failure { error } => usize.toI32(error.offset) + 100 - } + Result.Success { value } => usize.toI32(byteLength(value)) + Result.Failure { error } => usize.toI32(error.offset) + 100 } } @@ -213,14 +211,14 @@ import silk.string { scalarByteOffset, nextCursor } -import silk.option { Option, Some, None } +import silk.option { Option } import silk.char { toU32 as charToU32 } fn walk(value: string, cursor: ScalarCursor, expectedOffset: usize) -> u32 { if cursorByteOffset(&cursor) == expectedOffset {} else { return u32.toU32(1) } return match move nextScalar(value, move cursor) { - Some { value: step } => continueWalk(value, move step) - None nothing => u32.toU32(0) + Option.Some { value: step } => continueWalk(value, move step) + Option.None => u32.toU32(0) } } diff --git a/packages/compiler/test/TargetDependentConstants.test.ts b/packages/compiler/test/TargetDependentConstants.test.ts index 47882b0b3..fd8ad3595 100644 --- a/packages/compiler/test/TargetDependentConstants.test.ts +++ b/packages/compiler/test/TargetDependentConstants.test.ts @@ -241,14 +241,14 @@ it.effect('reports no diagnostic for the new stdlib declarations on either width const probes = [ 'fn usizeMaxIsAllOnes() -> i32 { if usize.MAX == usize.bitNot(usize.ZERO) { return 42 } return 0 }', 'fn usizeMaxWrapsToZero() -> i32 { if usize.wrappingAdd(usize.MAX, usize.ONE) == usize.ZERO { return 42 } return 0 }', - 'fn usizeMaxRefusesStep() -> i32 { return match move usize.checkedAdd(usize.MAX, usize.ONE) { Some { value: result } => 0 None nothing => 42 } }', + 'fn usizeMaxRefusesStep() -> i32 { return match move usize.checkedAdd(usize.MAX, usize.ONE) { Option.Some { value: result } => 0 _ => 42 } }', 'fn usizeMinIsZero() -> i32 { if usize.MIN == usize.ZERO { return 42 } return 0 }', - 'fn usizeMinRefusesStep() -> i32 { return match move usize.checkedSubtract(usize.MIN, usize.ONE) { Some { value: result } => 0 None nothing => 42 } }', + 'fn usizeMinRefusesStep() -> i32 { return match move usize.checkedSubtract(usize.MIN, usize.ONE) { Option.Some { value: result } => 0 _ => 42 } }', 'fn usizeBitsPinsMax() -> i32 { if usize.shiftRight(usize.MAX, usize.subtract(u32.toUsize(usize.BITS), usize.ONE)) == usize.ONE { return 42 } return 0 }', 'fn isizeMaxComplementsMin() -> i32 { if isize.MAX == isize.bitNot(isize.MIN) { return 42 } return 0 }', 'fn isizeMaxWrapsToMin() -> i32 { if isize.wrappingAdd(isize.MAX, i32.toIsize(1)) == isize.MIN { return 42 } return 0 }', - 'fn isizeMaxRefusesStep() -> i32 { return match move isize.checkedAdd(isize.MAX, i32.toIsize(1)) { Some { value: result } => 0 None nothing => 42 } }', - 'fn isizeMinRefusesStep() -> i32 { return match move isize.checkedSubtract(isize.MIN, i32.toIsize(1)) { Some { value: result } => 0 None nothing => 42 } }', + 'fn isizeMaxRefusesStep() -> i32 { return match move isize.checkedAdd(isize.MAX, i32.toIsize(1)) { Option.Some { value: result } => 0 _ => 42 } }', + 'fn isizeMinRefusesStep() -> i32 { return match move isize.checkedSubtract(isize.MIN, i32.toIsize(1)) { Option.Some { value: result } => 0 _ => 42 } }', 'fn isizeBitsMatchUsize() -> i32 { if isize.BITS == usize.BITS { return 42 } return 0 }', ] @@ -264,7 +264,7 @@ const acceptance = `import silk.i32 as i32 import silk.isize as isize import silk.u32 as u32 import silk.usize as usize -import silk.option { Some, None } +import silk.option { Option } ${probes.join('\n')} diff --git a/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts b/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts index d8b1b8cd6..64ee37966 100644 --- a/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts +++ b/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts @@ -36,7 +36,7 @@ import silk.filesystem { make as pathMake, rawBytes as pathRawBytes, release, releaseIgnored, removeDirectoryRecursively, temporaryDirectory, view as pathView } -import silk.result { Failure, Result, Success } +import silk.result { Result } struct Sentinel { code: i32 } @@ -52,13 +52,11 @@ import silk.result { Result } pub fn main() -> i32 { let completed = run Intrinsic.effectResult(program()) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { + Result.Success { value } => value + Result.Failure { error } => match move error { FileError failure => 100 + failure.reason.code OutOfMemoryError exhausted => 99 } - } } }` diff --git a/packages/compiler/test/UnicodeNormalizationConformance.test.ts b/packages/compiler/test/UnicodeNormalizationConformance.test.ts index cf8257e04..c63e0aaa2 100644 --- a/packages/compiler/test/UnicodeNormalizationConformance.test.ts +++ b/packages/compiler/test/UnicodeNormalizationConformance.test.ts @@ -104,7 +104,7 @@ import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.u8 as u8 import silk.usize as usize -import silk.result { Result, Success, Failure } +import silk.result { Result } import silk.string { String, InvalidUtf8, fromUtf8, ownedUtf8Bytes } import silk.unicode { normalizeNfc, normalizeNfd } import silk.vector { Vector, make as vectorMake, append as vectorAppend, asSlice as vectorAsSlice } @@ -173,10 +173,8 @@ effect fn build() -> i32 ! OutOfMemoryError { |> Effect.provideMut(&mut allocator) offset = offset + usize.ONE + nfdLength let sourceText = match move fromUtf8(vectorAsSlice(&sourceBytes)) { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => "" - } + Result.Success { value } => value + Result.Failure { error } => "" } let fromSource = run checkFrom( sourceText, @@ -184,10 +182,8 @@ effect fn build() -> i32 ! OutOfMemoryError { vectorAsSlice(&nfdBytes), ) |> Effect.provideMut(&mut allocator) let nfcText = match move fromUtf8(vectorAsSlice(&nfcBytes)) { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => "" - } + Result.Success { value } => value + Result.Failure { error } => "" } let fromNfc = run checkFrom( nfcText, @@ -195,10 +191,8 @@ effect fn build() -> i32 ! OutOfMemoryError { vectorAsSlice(&nfdBytes), ) |> Effect.provideMut(&mut allocator) let nfdText = match move fromUtf8(vectorAsSlice(&nfdBytes)) { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => "" - } + Result.Success { value } => value + Result.Failure { error } => "" } let fromNfd = run checkFrom( nfdText, diff --git a/packages/compiler/test/UserServices.test.ts b/packages/compiler/test/UserServices.test.ts index df56d36ed..1b5156f85 100644 --- a/packages/compiler/test/UserServices.test.ts +++ b/packages/compiler/test/UserServices.test.ts @@ -573,7 +573,7 @@ for (const provider of [ () => Effect.gen(function* () { const source = `import silk.effect as Effect -import silk.result { Result, Success, Failure } +import silk.result { Result } struct Token { value: i32 } service Counter { effect fn increment(token: ${provider.access === 'Exclusive' ? '&mut Token' : '&Token'}) -> i32 ? ${provider.access === 'Exclusive' ? '&mut Counter' : '&Counter'} @@ -625,10 +625,8 @@ pub fn main() -> i32 { let reifiedAlias = move reifiedHop let completed = run move reifiedAlias let second = match move completed { - Result { value: outcome } => match move outcome { - Success { value: answer } => answer - Failure { error: impossible } => 0 - } + Result.Success { value: answer } => answer + Result.Failure { error: impossible } => 0 } let branches = branchRead(true, 40) + branchRead(false, 50) diff --git a/packages/compiler/test/VectorAcceptance.test.ts b/packages/compiler/test/VectorAcceptance.test.ts index 96f46b59d..e5e3190d1 100644 --- a/packages/compiler/test/VectorAcceptance.test.ts +++ b/packages/compiler/test/VectorAcceptance.test.ts @@ -587,8 +587,7 @@ const popShrinks = `import silk.allocator { OutOfMemoryError } import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect -import silk.option { None } -import silk.option { Some } +import silk.option { Option } import silk.vector { Vector, make, append, get, length, capacity, pop } effect fn build() -> i32 ! OutOfMemoryError { @@ -602,8 +601,8 @@ effect fn build() -> i32 ! OutOfMemoryError { let appended2 = run pending2 let taken = pop(&mut values) let last = match move taken { - Some { value } => move value - None missing => 0 + Option.Some { value } => move value + Option.None => 0 } if last == 21 {} else { return 1 } // The length drops by exactly one and the capacity is untouched. @@ -625,8 +624,7 @@ it.effect( const popEmpty = `import silk.allocator { OutOfMemoryError } import silk.effect as Effect -import silk.option { None } -import silk.option { Some } +import silk.option { Option } import silk.vector { Vector, make, append, length, pop } effect fn build() -> i32 ! OutOfMemoryError { @@ -634,8 +632,8 @@ effect fn build() -> i32 ! OutOfMemoryError { // An empty vector never allocated, so pop must answer from the Empty arm. let first = pop(&mut values) let absent = match move first { - Some { value } => 0 - None missing => 42 + Option.Some { value } => 0 + Option.None => 42 } if length(&values) == 0 {} else { return 1 } return absent @@ -846,8 +844,7 @@ import silk.allocator { OutOfMemoryError } import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect -import silk.option { None } -import silk.option { Some } +import silk.option { Option } import silk.vector { Vector, make, append, length, pop, remove } struct Entry { @@ -886,8 +883,8 @@ effect fn build() -> i32 ! OutOfMemoryError { let s3 = run (seed(&mut values, 9) |> Effect.provideMut(&mut allocator)) let taken = pop(&mut values) let popped = match move taken { - Some { value } => release(move value) - None missing => 0 + Option.Some { value } => release(move value) + Option.None => 0 } // The popped element is the last one appended, and the removed one is the first. if popped == 9 {} else { return 1 } diff --git a/packages/compiler/test/VectorSort.test.ts b/packages/compiler/test/VectorSort.test.ts index e8bccba39..998a2fdd4 100644 --- a/packages/compiler/test/VectorSort.test.ts +++ b/packages/compiler/test/VectorSort.test.ts @@ -22,7 +22,7 @@ import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.usize as usize import silk.vector { Vector, make, append, sort, binarySearch, get, length } -import silk.option { Option, Some, None } +import silk.option { Option } effect fn build() -> i32 ! OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() @@ -233,9 +233,9 @@ ${fill('values', [9, 2, 7, 2, 5])} let hit = binarySearch(&values, 7) let miss = binarySearch(&values, 4) let duplicate = binarySearch(&values, 2) - let hitCode = match move hit { Some { value } => usize.toI32(value) None {} => 0 - 1 } - let missCode = match move miss { Some { value } => usize.toI32(value) None {} => 0 - 1 } - let duplicateCode = match move duplicate { Some { value } => usize.toI32(value) None {} => 0 - 1 } + let hitCode = match move hit { Option.Some { value } => usize.toI32(value) _ => 0 - 1 } + let missCode = match move miss { Option.Some { value } => usize.toI32(value) _ => 0 - 1 } + let duplicateCode = match move duplicate { Option.Some { value } => usize.toI32(value) _ => 0 - 1 } return hitCode * 100 + (missCode + 1) * 10 + duplicateCode`), ) assert.strictEqual(value, 300) @@ -253,9 +253,9 @@ ${fill('values', [5, 1, 9])} let between = binarySearch(&values, 4) let above = binarySearch(&values, 12) let mut score = 0 - let belowCode = match move below { Some { value } => 1 None {} => 0 } - let betweenCode = match move between { Some { value } => 1 None {} => 0 } - let aboveCode = match move above { Some { value } => 1 None {} => 0 } + let belowCode = match move below { Option.Some { value } => 1 _ => 0 } + let betweenCode = match move between { Option.Some { value } => 1 _ => 0 } + let aboveCode = match move above { Option.Some { value } => 1 _ => 0 } if belowCode + betweenCode + aboveCode == 0 { return 42 } return 0`), ) @@ -269,7 +269,7 @@ it.effect('searches an empty vector without matching', () => 'vector-sort/search-empty', program(` let mut values = make() let missing = binarySearch(&values, 3) - return match move missing { Some { value } => 0 None {} => 42 }`), + return match move missing { Option.Some { value } => 0 _ => 42 }`), ) assert.strictEqual(value, 42) }), diff --git a/packages/compiler/test/fixtures/scheduler-fiber/fork-child.silk b/packages/compiler/test/fixtures/scheduler-fiber/fork-child.silk index 75fa81061..a20e82af7 100644 --- a/packages/compiler/test/fixtures/scheduler-fiber/fork-child.silk +++ b/packages/compiler/test/fixtures/scheduler-fiber/fork-child.silk @@ -171,13 +171,12 @@ fn completeChild( completed: Result.Result, producer: Fiber.CompletionProducer, ) -> () { - let Result.Result { value } = move completed - return match move value { - Result.Success { value: success } => Fiber.completeSuccess( + return match move completed { + Result.Result.Success { value: success } => Fiber.completeSuccess( move producer, move success, ) - Result.Failure { error } => Fiber.completeFailure(move producer, move error) + Result.Result.Failure { error } => Fiber.completeFailure(move producer, move error) } } diff --git a/packages/compiler/test/support/corpus.ts b/packages/compiler/test/support/corpus.ts index f617b54d7..9b3eb4cd0 100644 --- a/packages/compiler/test/support/corpus.ts +++ b/packages/compiler/test/support/corpus.ts @@ -728,7 +728,7 @@ export const independentExecutionParkedTypedFailure = `import silk.allocator { A import silk.allocator { Allocator } import silk.effect as Effect import silk.execution as Execution -import silk.result { Result, Success, Failure } +import silk.result { Result } struct Failed { code: i32 } struct Empty {} struct Stored { execution: Intrinsic.Execution> } @@ -743,10 +743,8 @@ effect fn body() -> Result { fn ready(state: &()) -> () { return () } fn observe(result: Result) -> i32 { return match move result { - Result { value: outcome } => match move outcome { - Success { value } => value - Failure { error } => match move error { Failed { code } => code } - } + Result.Success { value } => value + Result.Failure { error } => match move error { Failed { code } => code } } } fn complete(owner: &mut Owner, result: Result) -> () { @@ -1718,13 +1716,13 @@ import silk.string { scalarValue, nextCursor } -import silk.option { Some, None } +import silk.option { Option } import silk.char { toU32 as charToU32 } fn scalarSum(value: string, cursor: ScalarCursor) -> u32 { return match move nextScalar(value, move cursor) { - Some { value: step } => continueSum(value, move step) - None nothing => u32.toU32(0) + Option.Some { value: step } => continueSum(value, move step) + Option.None => u32.toU32(0) } } @@ -1874,7 +1872,7 @@ import silk.i32 as i32 import silk.hash as Hash import silk.hash { HashKey, HashSeed, Word } import silk.hash_map { HashMap, bucketCount, contains, get, insert, length, make, remove } -import silk.option { Option, Some, None } +import silk.option { Option } effect fn build() -> i32 ! OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() diff --git a/packages/compiler/test/support/ownedAllocatorSuspension.ts b/packages/compiler/test/support/ownedAllocatorSuspension.ts index 7c35627db..2d0043075 100644 --- a/packages/compiler/test/support/ownedAllocatorSuspension.ts +++ b/packages/compiler/test/support/ownedAllocatorSuspension.ts @@ -4,7 +4,7 @@ import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.layout { Layout } -import silk.result { Result, Success, Failure } +import silk.result { Result } struct OwnedAllocator { storage: Allocation } effect fn allocate(self: &mut OwnedAllocator, layout: Layout) -> Allocation ! OutOfMemoryError { let mut inner = Allocator.systemAllocatorProvider() @@ -28,10 +28,8 @@ effect fn program() -> i32 ! OutOfMemoryError ? &mut Allocator { let inspected = inspect(protected()) let completed = run Intrinsic.bindRequirementOwned(move inspected, move provider) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => move value - Failure { error } => move error - } + Result.Success { value } => move value + Result.Failure { error } => move error } } effect fn outerRecover(error: OutOfMemoryError) -> i32 { return 9 } @@ -49,7 +47,7 @@ import silk.allocator { Allocator } import silk.allocator { SystemAllocator } import silk.effect as Effect import silk.layout { Layout } -import silk.result { Result, Success, Failure } +import silk.result { Result } struct Problem { code: i32 } struct OwnedAllocator { storage: Allocation } effect fn allocate(self: &mut OwnedAllocator, layout: Layout) -> Allocation ! OutOfMemoryError { @@ -77,10 +75,8 @@ effect fn program() -> i32 ! OutOfMemoryError ? &mut Allocator { let inspected = inspect(Intrinsic.catchFailure(protected(), recover)) let completed = run Intrinsic.bindRequirementOwned(move inspected, move provider) return match move completed { - Result { value: outcome } => match move outcome { - Success { value } => move value - Failure { error } => move error - } + Result.Success { value } => move value + Result.Failure { error } => move error } } effect fn outerRecover(error: OutOfMemoryError) -> i32 { return 9 } From 1d4fcdc5bcdec46b6c5b63f2d256c0fea98d6a4c Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 15:56:01 -0300 Subject: [PATCH 13/42] feat: make effect result carrier-neutral --- packages/compiler/src/BootstrapEffect.ts | 75 ++-- packages/compiler/src/BootstrapEvaluation.ts | 10 + packages/compiler/src/EffectLowering.ts | 325 +++++++----------- packages/compiler/src/Elaboration.ts | 4 +- packages/compiler/src/ExecutableOrigin.ts | 31 +- packages/compiler/src/ExpressionAnalysis.ts | 140 +++++++- packages/compiler/src/Forwarding.ts | 33 +- packages/compiler/src/Hir.ts | 8 +- packages/compiler/src/HirLowering.ts | 6 + .../compiler/src/InspectorProjectBackend.ts | 2 + .../compiler/src/InspectorProjectSyntax.ts | 7 +- packages/compiler/src/Instances.ts | 2 - packages/compiler/src/Intrinsic.ts | 10 +- packages/compiler/src/Layout.ts | 5 + packages/compiler/src/LowerExpression.ts | 8 +- packages/compiler/src/LowerStatements.ts | 5 +- packages/compiler/src/Mir.ts | 38 +- packages/compiler/src/MirEncoding.ts | 13 +- packages/compiler/src/MirLinearization.ts | 61 +++- packages/compiler/src/MirVerification.ts | 109 +++--- .../compiler/src/NativeEffectOperation.ts | 149 +++----- packages/compiler/src/Ownership.ts | 29 +- packages/compiler/src/ProvisionalMir.ts | 60 +--- packages/compiler/src/SemanticOccurrence.ts | 2 + packages/compiler/src/Stdlib.generated.ts | 21 +- packages/compiler/src/Suspension.ts | 12 +- packages/compiler/src/WasmBackend.ts | 73 ++-- packages/compiler/stdlib/silk/effect.silk | 4 +- packages/compiler/stdlib/silk/filesystem.silk | 6 +- packages/compiler/stdlib/silk/logger.silk | 3 +- .../compiler/stdlib/silk/os_filesystem.silk | 13 +- 31 files changed, 670 insertions(+), 594 deletions(-) diff --git a/packages/compiler/src/BootstrapEffect.ts b/packages/compiler/src/BootstrapEffect.ts index ff961d6f8..9458c72db 100644 --- a/packages/compiler/src/BootstrapEffect.ts +++ b/packages/compiler/src/BootstrapEffect.ts @@ -1,12 +1,6 @@ import type { LocalState, MachineRequest, Step } from './BootstrapMachine.js' import type { BlockedReason, TraceEvent } from './BootstrapTrace.js' -import type { - AggregateValue, - EffectOutcomeValue, - EffectValue, - UnionValue, - Value, -} from './BootstrapValue.js' +import type { EffectOutcomeValue, EffectValue, Value } from './BootstrapValue.js' import { repackFailurePayload } from './BootstrapValue.js' import type * as DeclarationFacts from './DeclarationFacts.js' import * as Mir from './Mir.js' @@ -652,51 +646,30 @@ export function* execute( throw new RangeError('MIR Effect result runner returned a non-outcome value') const outcome = result.value write(operation.outcome, { value: outcome, fromCall: true }) - const branch: AggregateValue = - outcome.tag === 0 - ? Object.freeze({ - _tag: 'AggregateValue', - type: operation.successType, - fields: Object.freeze([ - Object.freeze({ field: operation.successField, value: outcome.payload }), - ]), - }) - : (() => { - const failure = Type.failureCarrierMember( - operation.outcomeType.type, - outcome.tag, - 'OneBased', - ) - if (failure === undefined) - throw new RangeError('MIR Effect result has an invalid failure tag') - const failureValue: Value = Type.isUnion(operation.failureValueType) - ? Object.freeze({ - _tag: 'UnionValue', - type: operation.failureValueType, - member: failure, - payload: outcome.payload, - }) - : outcome.payload - return Object.freeze({ - _tag: 'AggregateValue' as const, - type: operation.failureType, - fields: Object.freeze([ - Object.freeze({ field: operation.failureField, value: failureValue }), - ]), - }) - })() - const outer: UnionValue = Object.freeze({ - _tag: 'UnionValue', - type: operation.resultUnion, - member: outcome.tag === 0 ? operation.successType : operation.failureType, - payload: branch, - }) - const completed: AggregateValue = Object.freeze({ - _tag: 'AggregateValue', - type: operation.resultType.type, - fields: Object.freeze([Object.freeze({ field: operation.resultField, value: outer })]), + write(operation.destination, { + value: Object.freeze({ + _tag: 'IntegerValue', + type: 'i32', + value: outcome.tag === 0 ? 1n : 0n, + }), + fromCall: true, }) - write(operation.destination, { value: completed, fromCall: true }) + if (outcome.tag === 0) { + write(operation.successValue, { value: outcome.payload, fromCall: true }) + break + } + const failure = Type.failureCarrierMember(operation.outcomeType.type, outcome.tag, 'OneBased') + if (failure === undefined) + throw new RangeError('MIR Effect result has an invalid failure tag') + const failureValue: Value = Type.isUnion(operation.failureValueType) + ? Object.freeze({ + _tag: 'UnionValue', + type: operation.failureValueType, + member: failure, + payload: outcome.payload, + }) + : outcome.payload + write(operation.failureValue, { value: failureValue, fromCall: true }) break } } diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index 3e5dc1774..fa95c5787 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -1956,6 +1956,16 @@ function* executeFunction( write(operation.destination, read(operation.right.result)) break } + case 'Conditional': { + const branch = + readInteger(operation.condition, 'i32').value === 0n + ? operation.otherwise + : operation.taken + const branchStep = yield* executeOperations(branch.operations) + if (branchStep !== undefined) return branchStep + write(operation.destination, read(branch.result)) + break + } case 'Match': { const scrutinee = read(operation.scrutinee).value let activeIdentity: Match.CoverageIdentity | undefined diff --git a/packages/compiler/src/EffectLowering.ts b/packages/compiler/src/EffectLowering.ts index 5e33a861c..4d1d49fb1 100644 --- a/packages/compiler/src/EffectLowering.ts +++ b/packages/compiler/src/EffectLowering.ts @@ -7,7 +7,6 @@ import { propagationReleases, } from './CleanupEmission.js' import * as ConformanceProof from './ConformanceProof.js' -import type * as DeclarationFacts from './DeclarationFacts.js' import type {} from './EntryAssembly.js' import type {} from './Forwarding.js' import { effectRecipe, inlineForwardedRequirement } from './Forwarding.js' @@ -312,14 +311,9 @@ export const lowerRunEffectComposite = ( } export interface ReifiedEffect { - readonly result: Mir.LocalId - readonly resultType: Extract - readonly resultField: DeclarationFacts.FieldId - readonly resultUnion: Type.StructuralUnion - readonly successType: Type.Nominal - readonly successField: DeclarationFacts.FieldId - readonly failureType: Type.Nominal - readonly failureField: DeclarationFacts.FieldId + readonly valid: Mir.LocalId + readonly success: Mir.LocalId + readonly failure: Mir.LocalId readonly failureValueType: Type.Type } @@ -327,7 +321,6 @@ export const reifyEffectValue = ( fn: FunctionLowering, effect: Mir.LocalId, effectType: Extract, - result: Type.Type, span: SourceSpan.SourceSpan, availableRequirements: ReadonlyArray = fn.providedRequirements, ): ReifiedEffect | undefined => { @@ -343,59 +336,35 @@ export const reifyEffectValue = ( _tag: 'EffectOutcome', type: effectType.type, }) - const resultType = fn.type(result) const failureValueType = Type.failureValue(Type.failureMembers(effectType.type)) - const successType = Type.resultSuccess(effectType.type.success) - const failureType = Type.resultFailure(failureValueType) - const resultUnionNormalization = Type.union([successType, failureType]) - const resultUnion = - resultUnionNormalization._tag === 'Normalized' && Type.isUnion(resultUnionNormalization.type) - ? resultUnionNormalization.type - : undefined - const resultEntry = - resultType?._tag === 'Nominal' ? Layout.entry(fn.layout, resultType.type) : undefined - const successEntry = Layout.entry(fn.layout, successType) - const failureEntry = Layout.entry(fn.layout, failureType) - const resultField = - resultEntry?.representation._tag === 'Aggregate' - ? resultEntry.representation.fields.at(0)?.id - : undefined - const successField = - successEntry?.representation._tag === 'Aggregate' - ? successEntry.representation.fields.at(0)?.id - : undefined - const failureField = - failureEntry?.representation._tag === 'Aggregate' - ? failureEntry.representation.fields.at(0)?.id - : undefined - const resultShape = - resultType?._tag === 'Nominal' ? Layout.callingShape(fn.layout, resultType.type) : undefined + const boolType = fn.type('bool') + const successType = fn.type(effectType.type.success) + const failureType = fn.type(failureValueType) const outcomeShape = Layout.callingShape(fn.layout, effectType.type) + const successShape = Layout.callingShape(fn.layout, effectType.type.success) const failureValueShape = Layout.callingShape(fn.layout, failureValueType) - const successTag = resultUnion?.members.findIndex((member) => Type.equals(member, successType)) - const failureTag = resultUnion?.members.findIndex((member) => Type.equals(member, failureType)) if ( - resultType?._tag !== 'Nominal' || - resultUnion === undefined || - resultField === undefined || - successField === undefined || - failureField === undefined || - resultShape === undefined || + boolType?._tag !== 'bool' || + successType === undefined || + successType._tag === 'EffectOutcome' || + (failureType?._tag !== 'Nominal' && failureType?._tag !== 'Union') || outcomeShape === undefined || + successShape === undefined || failureValueShape === undefined || - successTag === undefined || - successTag < 0 || - failureTag === undefined || - failureTag < 0 + Type.failureMembers(effectType.type).length === 0 ) return undefined const outcome = fn.alloc(outcomeType) - const destination = fn.alloc(resultType) + const valid = fn.alloc(boolType) + const success = fn.alloc(successType) + const failure = fn.alloc(failureType) fn.emit( Object.freeze({ _tag: 'ReifyEffect' as const, - destination, + destination: valid, outcome, + successValue: success, + failureValue: failure, effect, runner, runnerTypeArguments: @@ -403,32 +372,18 @@ export const reifyEffectValue = ( effectType.environment.instance.typeArguments, arguments: runtimeRequirementArguments(provided), outcomeType, - resultType, - resultField, - resultUnion, - successType, - successField, - successTag, - failureType, - failureField, - failureTag, failureValueType, - resultShape, + successShape, outcomeShape, failureValueShape, - type: resultType, + type: boolType, provenance: authored(span), }), ) return Object.freeze({ - result: destination, - resultType, - resultField, - resultUnion, - successType, - successField, - failureType, - failureField, + valid, + success, + failure, failureValueType, }) } @@ -441,6 +396,7 @@ export const callableEffectResult = ( const typeArguments = callable.environment?.callable.typeArguments ?? callable.storage?.realization.targetArguments ?? + callable.typeArguments ?? Object.freeze([]) const result = fn.effectResults.get(instanceText(callable.target.declaration, typeArguments)) return result?._tag === 'EffectValue' ? result : undefined @@ -524,32 +480,10 @@ export const lowerEffectCatch = ( ) return undefined - const reified = reifyEffectValue( - fn, - protected_.result, - protectedType, - Type.result(protectedEffect.success, Type.failureValue(protectedFailures)), - expression.span, - ) + const reified = reifyEffectValue(fn, protected_.result, protectedType, expression.span) if (reified === undefined) return undefined - const resultUnionType: Extract = Object.freeze({ - _tag: 'Union', - type: reified.resultUnion, - }) - const resultUnion = fn.alloc(resultUnionType) - fn.emit( - Object.freeze({ - _tag: 'Project' as const, - destination: resultUnion, - source: reified.result, - field: reified.resultField, - type: resultUnionType, - provenance: generated(expression.span), - }), - ) const successType = fn.type(resultEffect.success) - const resultUnionShape = Layout.callingShape(fn.layout, reified.resultUnion) const successShape = Layout.callingShape(fn.layout, resultEffect.success) const failureValueMir = fn.type(reified.failureValueType) const propagationEffect = fn.effectOutcome @@ -559,7 +493,6 @@ export const lowerEffectCatch = ( if ( successType === undefined || successType._tag === 'EffectOutcome' || - resultUnionShape === undefined || successShape === undefined || (failureValueMir?._tag !== 'Nominal' && failureValueMir?._tag !== 'Union') || propagationEffect === undefined || @@ -569,34 +502,6 @@ export const lowerEffectCatch = ( return undefined const declaration = fn.owner.function.declaration.id - const outerMatch: Match.MatchId = Object.freeze({ - _tag: 'MatchId', - function: declaration, - span: expression.span, - }) - const successArm: Match.ArmId = Object.freeze({ - _tag: 'MatchArmId', - match: outerMatch, - ordinal: 0, - }) - const failureArm: Match.ArmId = Object.freeze({ - _tag: 'MatchArmId', - match: outerMatch, - ordinal: 1, - }) - const successBinding: Match.BindingId = Object.freeze({ - _tag: 'PatternBindingId', - arm: successArm, - ordinal: 0, - }) - const failureBinding: Match.BindingId = Object.freeze({ - _tag: 'PatternBindingId', - arm: failureArm, - ordinal: 0, - }) - const success = fn.alloc(successType) - const failure = fn.alloc(failureValueMir) - const failureMembers = failureValueMir._tag === 'Nominal' ? Object.freeze([failureValueMir.type]) @@ -670,6 +575,7 @@ export const lowerEffectCatch = ( typeArguments: handlerType.environment?.callable.typeArguments ?? handlerType.storage?.realization.targetArguments ?? + handlerType.typeArguments ?? Object.freeze([]), captures: Object.freeze([]), arguments: Object.freeze([handlerArgument]), @@ -740,9 +646,9 @@ export const lowerEffectCatch = ( _tag: 'Match', id: innerMatch, destination: innerResult, - scrutinee: failure, + scrutinee: reified.failure, scrutineeType: failureValueMir, - scrutineeShape: Layout.callingShape(fn.layout, reified.failureValueType) ?? resultUnionShape, + scrutineeShape: Layout.callingShape(fn.layout, reified.failureValueType) ?? successShape, access: 'Move', retainsBindings: false, members: failureCoverage, @@ -762,77 +668,14 @@ export const lowerEffectCatch = ( const destination = fn.alloc(successType) fn.emit( Object.freeze({ - _tag: 'Match' as const, - id: outerMatch, + _tag: 'Conditional' as const, destination, - scrutinee: resultUnion, - scrutineeType: resultUnionType, - scrutineeShape: resultUnionShape, - access: 'Move' as const, - retainsBindings: false, - members: Object.freeze(reified.resultUnion.members.map(Match.structuralMember)), - decisions: Object.freeze( - reified.resultUnion.members.map((member) => - Object.freeze({ - member: Match.structuralMember(member), - candidates: Object.freeze([ - Type.equals(member, reified.successType) ? successArm : failureArm, - ]), - }), - ), - ), - arms: Object.freeze([ - Object.freeze({ - id: successArm, - member: Match.structuralMember(reified.successType), - universal: false, - before: Object.freeze(reified.resultUnion.members.map(Match.structuralMember)), - after: Object.freeze([Match.structuralMember(reified.failureType)]), - bindings: Object.freeze([ - Object.freeze({ - id: successBinding, - destination: success, - path: Object.freeze([reified.successField]), - type: successType, - access: 'Move' as const, - provenance: generated(expression.span), - }), - ]), - selected: Object.freeze({ - access: 'Move' as const, - operations: unusedHandlerDrop(), - result: success, - cleanup: Object.freeze([]), - endBorrow: false, - }), - provenance: generated(expression.span), - }), - Object.freeze({ - id: failureArm, - member: Match.structuralMember(reified.failureType), - universal: false, - before: Object.freeze([Match.structuralMember(reified.failureType)]), - after: Object.freeze([]), - bindings: Object.freeze([ - Object.freeze({ - id: failureBinding, - destination: failure, - path: Object.freeze([reified.failureField]), - type: failureValueMir, - access: 'Move' as const, - provenance: generated(expression.span), - }), - ]), - selected: Object.freeze({ - access: 'Move' as const, - operations: Object.freeze([innerOperation]), - result: innerResult, - cleanup: Object.freeze([]), - endBorrow: false, - }), - provenance: generated(expression.span), - }), - ]), + condition: reified.valid, + taken: Object.freeze({ operations: unusedHandlerDrop(), result: reified.success }), + otherwise: Object.freeze({ + operations: Object.freeze([innerOperation]), + result: innerResult, + }), type: successType, resultShape: successShape, provenance: generated(expression.span), @@ -1307,10 +1150,12 @@ const lowerForwardedProvider = ( export const lowerReifiedEffectRecipe = ( fn: FunctionLowering, subject: Hir.Expression, + successCarrier: Hir.Expression, + failureCarrier: Hir.Expression, resultType: Type.Type, span: SourceSpan.SourceSpan, availableRequirements: ReadonlyArray = fn.providedRequirements, -): ReifiedEffect | undefined => { +): LoweredExpression | undefined => { const recipe = effectRecipe(fn, subject) const forwarded = inlineForwardedRequirement(fn, recipe) if (forwarded !== undefined) { @@ -1318,6 +1163,8 @@ export const lowerReifiedEffectRecipe = ( const reified = lowerReifiedEffectRecipe( fn, forwarded.binding.protected, + successCarrier, + failureCarrier, resultType, span, Object.freeze([...availableRequirements, requirement]), @@ -1333,6 +1180,8 @@ export const lowerReifiedEffectRecipe = ( const reified = lowerReifiedEffectRecipe( fn, recipe.protected, + successCarrier, + failureCarrier, resultType, span, Object.freeze([...availableRequirements, requirement]), @@ -1348,19 +1197,91 @@ export const lowerReifiedEffectRecipe = ( : lowerExpression(fn, recipe) const effectType = lowered === undefined ? undefined : fn.localTypes.at(lowered.result.ordinal) if (lowered === undefined || effectType?._tag !== 'EffectValue') return undefined - const reified = reifyEffectValue( - fn, - lowered.result, - effectType, - resultType, - span, - availableRequirements, + const success = lowerExpression(fn, successCarrier) + const failure = lowerExpression(fn, failureCarrier) + const successType = success === undefined ? undefined : fn.localTypes.at(success.result.ordinal) + const failureType = failure === undefined ? undefined : fn.localTypes.at(failure.result.ordinal) + const carrierType = fn.type(resultType) + const carrierShape = + carrierType === undefined + ? undefined + : Layout.callingShape(fn.layout, Mir.semanticType(carrierType)) + if ( + success === undefined || + failure === undefined || + successType?._tag !== 'CallableValue' || + failureType?._tag !== 'CallableValue' || + carrierType === undefined || + carrierType._tag === 'EffectOutcome' || + carrierShape === undefined ) + return undefined + const reified = reifyEffectValue(fn, lowered.result, effectType, span, availableRequirements) if (reified === undefined) return undefined + const drop = ( + local: Mir.LocalId, + type: Extract, + ): ReadonlyArray => { + const cleanup = cleanupForLocal(fn, concreteCleanup(fn, Mir.semanticType(type)), type) + return cleanup._tag === 'NoCleanup' + ? Object.freeze([]) + : Object.freeze([ + Object.freeze({ + _tag: 'Drop' as const, + local, + cleanup, + provenance: generated(span), + }), + ]) + } + const apply = ( + callable: Mir.LocalId, + callableType: Extract, + argument: Mir.LocalId, + ): Extract => + Object.freeze({ + _tag: 'ApplyCallable', + destination: fn.alloc(carrierType), + callable, + typeArguments: + callableType.environment?.callable.typeArguments ?? + callableType.storage?.realization.targetArguments ?? + callableType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: Object.freeze([argument]), + callableType: callableType.type, + access: callableType.type.mode, + evaluation: 'CalleeThenArguments', + realization: 'Environment', + type: carrierType, + provenance: generated(span), + }) + const successApply = apply(success.result, successType, reified.success) + const failureApply = apply(failure.result, failureType, reified.failure) + const destination = fn.alloc(carrierType) + fn.emit( + Object.freeze({ + _tag: 'Conditional' as const, + destination, + condition: reified.valid, + taken: Object.freeze({ + operations: Object.freeze([...drop(failure.result, failureType), successApply]), + result: successApply.destination, + }), + otherwise: Object.freeze({ + operations: Object.freeze([...drop(success.result, successType), failureApply]), + result: failureApply.destination, + }), + type: carrierType, + resultShape: carrierShape, + provenance: generated(span), + }), + ) endRunLoans(fn, span) if (recipe._tag === 'EffectConstruct' || recipe._tag === 'ServiceEffectConstruct') endLoans(fn, recipe.loanEnds, span) - return reified + return Object.freeze({ result: destination }) } export const lowerEffectExecution = ( diff --git a/packages/compiler/src/Elaboration.ts b/packages/compiler/src/Elaboration.ts index 4643bcc55..0cfa5dda3 100644 --- a/packages/compiler/src/Elaboration.ts +++ b/packages/compiler/src/Elaboration.ts @@ -931,10 +931,12 @@ export type ExpressionFact = readonly syntax: SyntaxTree.Node } | { - /** Executes one typed Effect into ordinary Result data without catching traps. */ + /** Folds one typed Effect outcome through ordinary carrier functions without catching traps. */ readonly _tag: 'EffectResult' readonly reference: IntrinsicReferenceFact readonly protected: ExpressionFact + readonly success: ExpressionFact + readonly failure: ExpressionFact readonly type: ExpressionTypeFact readonly syntax: SyntaxTree.Node } diff --git a/packages/compiler/src/ExecutableOrigin.ts b/packages/compiler/src/ExecutableOrigin.ts index 4e4fa2f7f..ab41bc569 100644 --- a/packages/compiler/src/ExecutableOrigin.ts +++ b/packages/compiler/src/ExecutableOrigin.ts @@ -290,7 +290,11 @@ export const make = (operations: Operations) => { ): ReadonlyArray => { if (expression._tag === 'Run') return callTargets(expression.subject, index, substitution) if (expression._tag === 'EffectResult') - return callTargets(expression.protected, index, substitution) + return [ + ...callTargets(expression.protected, index, substitution), + ...callTargets(expression.success, index, substitution), + ...callTargets(expression.failure, index, substitution), + ] if (expression._tag === 'EffectCatch') return [ ...callTargets(expression.protected, index, substitution), @@ -962,13 +966,18 @@ export const make = (operations: Operations) => { const completed = block._tag === 'EffectBlock' ? block.statements.at(-1) : undefined const run = completed?._tag === 'Return' ? completed.expression : undefined const result = run?._tag === 'Run' ? run.subject : undefined - const protected_ = result?._tag === 'EffectResult' ? result.protected : undefined - const parameter = protected_?._tag === 'Move' ? protected_.subject : protected_ - return block._tag === 'EffectBlock' && - block.statements.length === 1 && - parameter?._tag === 'ParameterReference' - ? parameter.parameter.ordinal - : undefined + if ( + block._tag !== 'EffectBlock' || + block.statements.length !== 1 || + result?._tag !== 'EffectResult' + ) + return undefined + const parameterOrdinal = (expression: Hir.Expression): number | undefined => { + const parameter = expression._tag === 'Move' ? expression.subject : expression + return parameter._tag === 'ParameterReference' ? parameter.parameter.ordinal : undefined + } + const protected_ = parameterOrdinal(result.protected) + return protected_ } const requirementBoundEffectRecipe = ( @@ -1588,7 +1597,6 @@ export const make = (operations: Operations) => { const targetFn = targetFunction(results, target.declaration) if (targetFn === undefined) return [] const resultEffect = resultEffectIdentity(targetFn, target, results, index) - const reifiedParameter = forwardedEffectResultParameter(targetFn) return [ Object.freeze({ _tag: 'CallInstance', @@ -1596,9 +1604,6 @@ export const make = (operations: Operations) => { span: expression.span, target, ...(resultEffect === undefined ? {} : { resultEffect }), - ...(reifiedParameter === undefined - ? {} - : { effectResultParameter: reifiedParameter }), }), ] } @@ -1612,7 +1617,6 @@ export const make = (operations: Operations) => { target === undefined ? undefined : targetFunction(results, expression.target) if (target === undefined || targetFn === undefined) return [] const resultEffect = resultEffectIdentity(targetFn, target, results, index) - const reifiedParameter = forwardedEffectResultParameter(targetFn) return [ Object.freeze({ _tag: 'CallInstance', @@ -1620,7 +1624,6 @@ export const make = (operations: Operations) => { span: expression.span, target, ...(resultEffect === undefined ? {} : { resultEffect }), - ...(reifiedParameter === undefined ? {} : { effectResultParameter: reifiedParameter }), }), ] }) diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index ce8230143..8051516e6 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -5043,6 +5043,7 @@ export const analyzeEffectResult = ( declaration: DeclarationFact, scope: Scope, resolution: ResolutionContext, + expected?: SemanticType, ): ExpressionResult => { const pipelined = node.kind === 'PipelineExpression' const target = pipelined ? (pipelineCallable(node) ?? node) : node @@ -5052,6 +5053,9 @@ export const analyzeEffectResult = ( isRecursiveArgumentNode(element), ) ?? [] const protectedNode = pipelined ? pipelineInput(node) : argumentNodes.at(0) + const successNode = argumentNodes.at(pipelined ? 0 : 1) + const failureNode = argumentNodes.at(pipelined ? 1 : 2) + const callTypeArguments = analyzeCallTypeArguments(source, target, declaration, resolution) const protectedResult = protectedNode === undefined ? undefined @@ -5060,10 +5064,83 @@ export const analyzeEffectResult = ( protectedResult?.type !== undefined && Type.isEffect(protectedResult.type) ? protectedResult.type : undefined - const diagnostics: Array = [...(protectedResult?.diagnostics ?? [])] - if (argumentNodes.length !== (pipelined ? 0 : 1)) + const expectedEffect = expected !== undefined && Type.isEffect(expected) ? expected : undefined + let carrierResult = expectedEffect?.success ?? callTypeArguments.types?.at(0) + let successResult = + successNode === undefined + ? undefined + : analyzeExpression( + source, + successNode, + declarations, + declaration, + scope, + resolution, + protectedEffect === undefined || carrierResult === undefined + ? undefined + : Type.callable(Object.freeze([protectedEffect.success]), carrierResult, 'Take'), + ) + let failureResult = + failureNode === undefined + ? undefined + : analyzeExpression( + source, + failureNode, + declarations, + declaration, + scope, + resolution, + protectedEffect === undefined || carrierResult === undefined + ? undefined + : Type.callable( + Object.freeze([Type.failureType(protectedEffect)]), + carrierResult, + 'Take', + ), + ) + const successCallable = + successResult?.type !== undefined && Type.isCallable(successResult.type) + ? successResult.type + : undefined + const failureCallable = + failureResult?.type !== undefined && Type.isCallable(failureResult.type) + ? failureResult.type + : undefined + carrierResult ??= successCallable?.result ?? failureCallable?.result + if (protectedEffect !== undefined && carrierResult !== undefined) { + if (successNode !== undefined) + successResult = analyzeExpression( + source, + successNode, + declarations, + declaration, + scope, + resolution, + Type.callable(Object.freeze([protectedEffect.success]), carrierResult, 'Take'), + ) + if (failureNode !== undefined) + failureResult = analyzeExpression( + source, + failureNode, + declarations, + declaration, + scope, + resolution, + Type.callable(Object.freeze([Type.failureType(protectedEffect)]), carrierResult, 'Take'), + ) + } + const diagnostics: Array = [ + ...callTypeArguments.diagnostics, + ...(protectedResult?.diagnostics ?? []), + ...(successResult?.diagnostics ?? []), + ...(failureResult?.diagnostics ?? []), + ] + if (argumentNodes.length !== (pipelined ? 2 : 3)) diagnostics.push( - Diagnostic.invalidEffectHandler('result requires exactly one Effect', node.span), + Diagnostic.invalidEffectHandler( + 'result requires one Effect plus success and failure carriers', + node.span, + ), ) if (protectedEffect === undefined) diagnostics.push( @@ -5072,15 +5149,52 @@ export const analyzeEffectResult = ( protectedNode?.span ?? node.span, ), ) - const failureValue = protectedEffect === undefined ? 'never' : Type.failureType(protectedEffect) + const expectedSuccess = + protectedEffect === undefined || carrierResult === undefined + ? undefined + : Type.callable(Object.freeze([protectedEffect.success]), carrierResult, 'Take') + const expectedFailure = + protectedEffect === undefined || carrierResult === undefined + ? undefined + : Type.callable(Object.freeze([Type.failureType(protectedEffect)]), carrierResult, 'Take') + if ( + expectedSuccess !== undefined && + (successResult?.type === undefined || !typesCompatible(successResult.type, expectedSuccess)) + ) + diagnostics.push( + Diagnostic.invalidEffectHandler( + 'the success carrier must accept the Effect success and return the shared result type', + successNode?.span ?? node.span, + ), + ) + if ( + expectedFailure !== undefined && + (failureResult?.type === undefined || !typesCompatible(failureResult.type, expectedFailure)) + ) + diagnostics.push( + Diagnostic.invalidEffectHandler( + 'the failure carrier must accept the Effect failure and return the shared result type', + failureNode?.span ?? node.span, + ), + ) const type = - protectedEffect === undefined + protectedEffect === undefined || carrierResult === undefined ? unavailableExpressionType : availableExpressionType( Type.effectWithRows( - Type.result(protectedEffect.success, failureValue), + carrierResult, RowAlgebra.concrete(Type.failureRowPolicy(), []), - protectedEffect.access, + strongestEffectAccess( + protectedResult === undefined + ? protectedEffect.access + : effectExpressionAccess(protectedResult.fact, resolution.index), + ...(successResult === undefined + ? [] + : [effectExpressionAccess(successResult.fact, resolution.index)]), + ...(failureResult === undefined + ? [] + : [effectExpressionAccess(failureResult.fact, resolution.index)]), + ), protectedEffect.requirementRow, ), ) @@ -5089,6 +5203,8 @@ export const analyzeEffectResult = ( _tag: 'EffectResult', reference: intrinsicReference(source, target), protected: protectedResult?.fact ?? unavailableExpression(node), + success: successResult?.fact ?? unavailableExpression(successNode ?? node), + failure: failureResult?.fact ?? unavailableExpression(failureNode ?? node), type, syntax: node, }), @@ -5842,7 +5958,15 @@ export function analyzeExpression( if (node.kind === 'PipelineExpression' || node.kind === 'CallExpression') { const operationTarget = node.kind === 'PipelineExpression' ? pipelineCallable(node) : node if (operationTarget !== undefined && isEffectResultTarget(source, operationTarget)) - return analyzeEffectResult(source, node, declarations, declaration, scope, resolution) + return analyzeEffectResult( + source, + node, + declarations, + declaration, + scope, + resolution, + expected, + ) if (node.kind === 'PipelineExpression') return analyzePipelineExpression(source, node, declarations, declaration, scope, resolution) } diff --git a/packages/compiler/src/Forwarding.ts b/packages/compiler/src/Forwarding.ts index 07748d785..26bd99d45 100644 --- a/packages/compiler/src/Forwarding.ts +++ b/packages/compiler/src/Forwarding.ts @@ -253,36 +253,6 @@ export const callableRecipe = ( return argument === undefined ? undefined : callableRecipe(fn, argument, resolving) } -export const inlineForwardedEffectResult = ( - fn: FunctionLowering, - expression: Hir.Expression, -): Extract | undefined => { - if (expression._tag !== 'EffectConstruct') return undefined - const call = fn.call(expression.span) - const parameter = call?.effectResultParameter - const protected_ = parameter === undefined ? undefined : expression.arguments.at(parameter) - let recipeBinding: number | undefined - if (protected_?._tag === 'BindingReference') { - recipeBinding = protected_.binding.ordinal - } else if (protected_?._tag === 'Move' && protected_.subject._tag === 'BindingReference') { - recipeBinding = protected_.subject.binding.ordinal - } else { - recipeBinding = undefined - } - const type = fn.semantic(expression.type) - return protected_ === undefined || - recipeBinding === undefined || - !fn.effectRecipes.has(recipeBinding) || - !Type.isEffect(type) - ? undefined - : Object.freeze({ - _tag: 'EffectResult', - protected: protected_, - type, - span: expression.span, - }) -} - export const effectRecipe = ( fn: FunctionLowering, expression: Hir.Expression, @@ -300,8 +270,7 @@ export const effectRecipe = ( const subject = effectRecipe(fn, expression.subject, resolving) return subject === expression.subject ? expression : subject } - const forwarded = inlineForwardedEffectResult(fn, expression) - return forwarded === undefined ? expression : effectRecipe(fn, forwarded, resolving) + return expression } export const movedEffectRecipe = ( diff --git a/packages/compiler/src/Hir.ts b/packages/compiler/src/Hir.ts index 11b226462..a9138e407 100644 --- a/packages/compiler/src/Hir.ts +++ b/packages/compiler/src/Hir.ts @@ -735,6 +735,8 @@ export type Expression = | { readonly _tag: 'EffectResult' readonly protected: Expression + readonly success: Expression + readonly failure: Expression readonly type: Type.Effect readonly span: SourceSpan.SourceSpan } @@ -1046,7 +1048,7 @@ export const expressionChildren = (expression: Expression): ReadonlyArray { return [ `${indent}effect-result : ${Type.encode(expression.type)} ${spanText(expression.span)}`, encodeExpression(expression.protected, depth + 1), + encodeExpression(expression.success, depth + 1), + encodeExpression(expression.failure, depth + 1), ].join('\n') case 'EffectCatch': return [ diff --git a/packages/compiler/src/HirLowering.ts b/packages/compiler/src/HirLowering.ts index 09c7adf08..d9818cd16 100644 --- a/packages/compiler/src/HirLowering.ts +++ b/packages/compiler/src/HirLowering.ts @@ -575,8 +575,12 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. } if (fact._tag === 'EffectResult') { const protected_ = hirExpression(fact.protected) + const success = hirExpression(fact.success) + const failure = hirExpression(fact.failure) if ( protected_._tag === 'Unavailable' || + success._tag === 'Unavailable' || + failure._tag === 'Unavailable' || fact.type._tag !== 'Available' || !Type.isEffect(fact.type.type) ) @@ -584,6 +588,8 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. return Object.freeze({ _tag: 'EffectResult', protected: protected_, + success, + failure, type: fact.type.type, span: fact.syntax.span, }) diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index 2c4dfd1c2..3c0ac4e5b 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -885,6 +885,8 @@ const operationLabel = (operation: Mir.Operation): string => { return `drop ${localText(operation.local)}` case 'Match': return `${localText(operation.destination)} = match ${operation.access.toLowerCase()} ${localText(operation.scrutinee)}` + case 'Conditional': + return `${localText(operation.destination)} = if ${localText(operation.condition)}` case 'ShortCircuit': return `${localText(operation.destination)} = ${operation.operator === 'And' ? '&&' : '||'} ${localText(operation.left)}` case 'HostWrite': diff --git a/packages/compiler/src/InspectorProjectSyntax.ts b/packages/compiler/src/InspectorProjectSyntax.ts index 4abd12189..03eaea5bc 100644 --- a/packages/compiler/src/InspectorProjectSyntax.ts +++ b/packages/compiler/src/InspectorProjectSyntax.ts @@ -315,7 +315,12 @@ export const hirRows = (hir: Hir.Module): ReadonlyArray => { expression(argument, depth + 1, `${path}.argument${index}`) }) } - if (node._tag === 'EffectResult' || node._tag === 'EffectBindRequirement') { + if (node._tag === 'EffectResult') { + expression(node.protected, depth + 1, `${path}.protected`) + expression(node.success, depth + 1, `${path}.success`) + expression(node.failure, depth + 1, `${path}.failure`) + } + if (node._tag === 'EffectBindRequirement') { expression(node.protected, depth + 1, `${path}.protected`) } if (node._tag === 'SliceLength') expression(node.slice, depth + 1, `${path}.s`) diff --git a/packages/compiler/src/Instances.ts b/packages/compiler/src/Instances.ts index 8bfce208e..2db858e55 100644 --- a/packages/compiler/src/Instances.ts +++ b/packages/compiler/src/Instances.ts @@ -132,8 +132,6 @@ export interface CallInstance { readonly span: Hir.Expression['span'] readonly target: InstanceKey readonly resultEffect?: string - /** Parameter whose Effect is reified by an exact source forwarding wrapper. */ - readonly effectResultParameter?: number } /** One exact sealed intrinsic call retained by executable instance closure. */ diff --git a/packages/compiler/src/Intrinsic.ts b/packages/compiler/src/Intrinsic.ts index 90687722e..5080d1102 100644 --- a/packages/compiler/src/Intrinsic.ts +++ b/packages/compiler/src/Intrinsic.ts @@ -1616,9 +1616,13 @@ const intrinsicOperations = Object.freeze([ effect({ name: 'result', operation: 'Result', - typeParameters: Object.freeze([]), - parameters: Object.freeze([valueParameter('protected', 'Effect')]), - result: 'Effect ? R>', + typeParameters: Object.freeze(['R']), + parameters: Object.freeze([ + valueParameter('protected', 'Effect'), + valueParameter('success', 'once fn(A) -> R'), + valueParameter('failure', 'once fn(E) -> R'), + ]), + result: 'Effect', }), contractEffect({ name: 'bindRequirement', diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 533e41652..7212465d4 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -2838,6 +2838,11 @@ export const plan = ( for (const expression of instance.function.statements .flatMap(Hir.statementExpressions) .flatMap(Hir.expressionTree)) { + if (expression._tag === 'EffectResult') { + reached.set(Type.key('bool'), 'bool') + const result = Type.substitute(expression.type, instance.substitution) + reached.set(Type.key(result), result) + } if ( expression._tag !== 'BuiltinCall' || (expression.operation !== 'ExecutionLayout' && diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 4d4c62f81..d062aade5 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -413,7 +413,9 @@ export function lowerExpressionInner( _tag: 'MakeCallable', destination, target: expression.target, - typeArguments: expression.typeArguments, + typeArguments: Object.freeze( + expression.typeArguments.map((argument) => fn.semanticArgument(argument)), + ), captures: Object.freeze([]), type, provenance: authored(expression.span), @@ -766,10 +768,12 @@ export function lowerExpressionInner( const reified = lowerReifiedEffectRecipe( fn, resultRecipe.protected, + resultRecipe.success, + resultRecipe.failure, expression.type, expression.span, ) - return reified === undefined ? undefined : Object.freeze({ result: reified.result }) + return reified } if ( resultRecipe !== undefined && diff --git a/packages/compiler/src/LowerStatements.ts b/packages/compiler/src/LowerStatements.ts index a3e3565a3..cdb973dbc 100644 --- a/packages/compiler/src/LowerStatements.ts +++ b/packages/compiler/src/LowerStatements.ts @@ -28,7 +28,6 @@ import { callableRecipe, delayedEffectState, effectRecipe, - inlineForwardedEffectResult, inlineForwardedRequirement, movedEffectRecipe, restoreDelayedEffectState, @@ -341,7 +340,6 @@ export const lowerSequence = ( : id } const forwardedRequirement = inlineForwardedRequirement(fn, statement.initializer) - const forwardedResult = inlineForwardedEffectResult(fn, statement.initializer) const forwardedResultEffect = forwardedRequirement === undefined ? undefined @@ -357,7 +355,6 @@ export const lowerSequence = ( protectedRecipe?._tag === 'ServiceEffectConstruct' || protectedRecipe !== forwardedRequirement.binding.protected) if ( - forwardedResult !== undefined || forwardedRequirementNeedsRecipe || statement.initializer._tag === 'ServiceEffectConstruct' || (statement.initializer._tag === 'EffectConstruct' && @@ -374,7 +371,7 @@ export const lowerSequence = ( effectContract(initializerType ?? 'never') !== undefined) || (statement.initializer._tag === 'BuiltinCall' && Type.isEffect(statement.initializer.type)) ) { - fn.effectRecipes.set(statement.binding.ordinal, forwardedResult ?? statement.initializer) + fn.effectRecipes.set(statement.binding.ordinal, statement.initializer) const following = fn.reserve() fn.publish( Object.freeze({ diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index 1163bd39f..5285b816d 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -975,29 +975,22 @@ export type Operation = readonly provenance: Provenance } | { - /** Runs one Effect and materializes only its completed typed channel as silk/result data. */ + /** Runs one Effect and exposes its completed typed channel without choosing a carrier. */ readonly _tag: 'ReifyEffect' readonly destination: LocalId readonly outcome: LocalId + readonly successValue: LocalId + readonly failureValue: LocalId readonly effect: LocalId readonly runner: DeclarationFacts.CanonicalId readonly runnerTypeArguments: ReadonlyArray readonly arguments: ReadonlyArray readonly outcomeType: Extract - readonly resultType: Extract - readonly resultField: DeclarationFacts.FieldId - readonly resultUnion: SilkType.StructuralUnion - readonly successType: SilkType.Nominal - readonly successField: DeclarationFacts.FieldId - readonly successTag: number - readonly failureType: SilkType.Nominal - readonly failureField: DeclarationFacts.FieldId - readonly failureTag: number readonly failureValueType: SilkType.Type - readonly resultShape: Layout.CallingShape + readonly successShape: Layout.CallingShape readonly outcomeShape: Layout.CallingShape readonly failureValueShape: Layout.CallingShape - readonly type: Extract + readonly type: Extract readonly provenance: Provenance } | { @@ -1095,8 +1088,27 @@ export type Operation = } | DropOperation | MatchOperation + | ConditionalOperation | ShortCircuitOperation +/** One compiler-owned structured conditional whose branches may produce any value type. */ +export interface ConditionalOperation { + readonly _tag: 'Conditional' + readonly destination: LocalId + readonly condition: LocalId + readonly taken: { + readonly operations: ReadonlyArray + readonly result: LocalId + } + readonly otherwise: { + readonly operations: ReadonlyArray + readonly result: LocalId + } + readonly type: Exclude + readonly resultShape: Layout.CallingShape + readonly provenance: Provenance +} + /** * One compiler-owned conditional evaluation: `&&` and `||`. `left` is already evaluated when the * operation runs; `right` holds the operations that evaluate the right operand and the local @@ -1642,6 +1654,8 @@ export const operationsOf = (region: Region): ReadonlyArray => { } export const operationChildren = (operation: Operation): ReadonlyArray => { + if (operation._tag === 'Conditional') + return [...operation.taken.operations, ...operation.otherwise.operations] if (operation._tag === 'ShortCircuit') return operation.right.operations if (operation._tag === 'Match') { return operation.arms.flatMap((arm) => [ diff --git a/packages/compiler/src/MirEncoding.ts b/packages/compiler/src/MirEncoding.ts index 75e4a6386..fc4a56086 100644 --- a/packages/compiler/src/MirEncoding.ts +++ b/packages/compiler/src/MirEncoding.ts @@ -197,6 +197,8 @@ const operationText = (operation: Operation): string => { return `drop ${localText(operation.local)}${operation.cleanup._tag === 'NoCleanup' ? '' : ` cleanup=${operation.cleanup._tag}`}${operation.localShared === undefined ? '' : ` element=${SilkType.encode(operation.localShared.element)} layout=${operation.localShared.block.provenance} transition=decrement-or-cleanup-release`} ${provenanceText(operation.provenance)}` case 'Match': return `${localText(operation.destination)} = match#${operation.id.span.start} ${operation.access.toLowerCase()} ${localText(operation.scrutinee)} : ${typeText(operation.scrutineeType)} -> ${typeText(operation.type)}${operation.retainsBindings ? ' retain-bindings' : ''} ${provenanceText(operation.provenance)}` + case 'Conditional': + return `${localText(operation.destination)} = conditional ${localText(operation.condition)} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` case 'ShortCircuit': return `${localText(operation.destination)} = short-circuit ${operation.operator === 'And' ? '&&' : '||'} ${localText(operation.left)} : bool ${provenanceText(operation.provenance)}` } @@ -206,6 +208,15 @@ const fieldPathText = (path: ReadonlyArray): string => path.length === 0 ? 'payload' : path.map((field) => `#${field.ordinal}`).join('.') const operationLines = (operation: Operation, indent: string): ReadonlyArray => { + if (operation._tag === 'Conditional') { + return [ + `${indent}${operationText(operation)}`, + `${indent} taken -> ${localText(operation.taken.result)}`, + ...operation.taken.operations.flatMap((child) => operationLines(child, `${indent} `)), + `${indent} otherwise -> ${localText(operation.otherwise.result)}`, + ...operation.otherwise.operations.flatMap((child) => operationLines(child, `${indent} `)), + ] + } if (operation._tag === 'ShortCircuit') { return [ `${indent}${operationText(operation)}`, @@ -328,7 +339,7 @@ const suspensionLines = (fn: MirFunction): ReadonlyArray => { ...suspensionRunnerLines(region.runner), region.completion._tag === 'Propagate' ? ` completion propagate outcome=${SilkType.encode(region.completion.outcome)} mappings=${region.completion.failureMappings.map((mapping) => `${mapping.source}:${mapping.target}`).join(',') || 'none'}` - : ` completion reify outcome=${SilkType.encode(region.completion.outcome)} result=${SilkType.encode(region.completion.resultType)} success-tag=${region.completion.successTag} failure-tag=${region.completion.failureTag}`, + : ` completion reify outcome=${SilkType.encode(region.completion.outcome)} success=${SilkType.encode(region.completion.successType)} failure=${SilkType.encode(region.completion.failureValueType)}`, ` live ${region.liveLocals.map(localText).join(',') || 'none'}`, ...(descriptor === undefined ? [] diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index 3b6ed9a80..d77eb5874 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -39,7 +39,12 @@ export type LinearOperation = | Exclude< Mir.Operation, { - readonly _tag: 'Match' | 'ShortCircuit' | 'CheckedScalar' | 'PropagateEffectFailure' + readonly _tag: + | 'Match' + | 'Conditional' + | 'ShortCircuit' + | 'CheckedScalar' + | 'PropagateEffectFailure' } > | { @@ -65,6 +70,7 @@ export const isLinearOperation = ( operation: Mir.Operation | LinearOperation, ): operation is LinearOperation => operation._tag !== 'Match' && + operation._tag !== 'Conditional' && operation._tag !== 'ShortCircuit' && operation._tag !== 'CheckedScalar' && operation._tag !== 'PropagateEffectFailure' @@ -225,6 +231,7 @@ export const expandMatches = ( const specialIndex = operations.findIndex( (operation) => operation._tag === 'Match' || + operation._tag === 'Conditional' || operation._tag === 'ShortCircuit' || operation._tag === 'CheckedScalar' || operation._tag === 'PropagateEffectFailure', @@ -337,6 +344,58 @@ export const expandMatches = ( ) return } + if (special?._tag === 'Conditional') { + const following = reserve() + const taken = reserve() + const otherwise = reserve() + blocks.push( + Object.freeze({ + id, + origin, + kind, + operations: linearOperations(operations.slice(0, specialIndex)), + terminator: Object.freeze({ + _tag: 'Branch', + condition: special.condition, + taken, + otherwise, + provenance: special.provenance, + }), + }), + ) + lowerSequence(following, origin, kind, operations.slice(specialIndex + 1), terminator) + lowerSequence( + taken, + origin, + 'Normal', + [ + ...special.taken.operations, + Object.freeze({ + _tag: 'Move' as const, + destination: special.destination, + source: special.taken.result, + provenance: special.provenance, + }), + ], + jump(following, special.provenance), + ) + lowerSequence( + otherwise, + origin, + 'Normal', + [ + ...special.otherwise.operations, + Object.freeze({ + _tag: 'Move' as const, + destination: special.destination, + source: special.otherwise.result, + provenance: special.provenance, + }), + ], + jump(following, special.provenance), + ) + return + } if (special?._tag === 'ShortCircuit') { const following = reserve() const evaluateRight = reserve() diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index ef6c07831..2b56f9285 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -123,6 +123,11 @@ const structuredCfgPathsValid = ( } return incoming } + if (operation._tag === 'Conditional') + return semantics.merge( + sequence(operation.taken.operations, incoming), + sequence(operation.otherwise.operations, incoming), + ) if (operation._tag === 'ShortCircuit') return semantics.merge(incoming, sequence(operation.right.operations, incoming)) if (operation._tag === 'Match') { @@ -522,10 +527,11 @@ const suspensionViolations = (fn: MirFunction, layout: Layout.Plan): ReadonlyArr }))) || (region.completion._tag === 'Reify' && (effectOperation._tag !== 'ReifyEffect' || - !SilkType.equals(region.completion.resultType, effectOperation.resultType.type) || - region.completion.resultField.ordinal !== effectOperation.resultField.ordinal || - region.completion.successTag !== effectOperation.successTag || - region.completion.failureTag !== effectOperation.failureTag)) + !SilkType.equals( + region.completion.successType, + effectOperation.outcomeType.type.success, + ) || + !SilkType.equals(region.completion.failureValueType, effectOperation.failureValueType))) ) invalid('InvalidSuspension', 'typed completion mapping disagrees with its MIR operation') } @@ -808,7 +814,14 @@ export const operationLocals = (operation: Operation): ReadonlyArray => ...operation.arguments, ] case 'ReifyEffect': - return [operation.destination, operation.outcome, operation.effect, ...operation.arguments] + return [ + operation.destination, + operation.outcome, + operation.successValue, + operation.failureValue, + operation.effect, + ...operation.arguments, + ] case 'CloseEffectEntry': return [ operation.destination, @@ -841,6 +854,13 @@ export const operationLocals = (operation: Operation): ReadonlyArray => arm.selected.result, ]), ] + case 'Conditional': + return [ + operation.destination, + operation.condition, + operation.taken.result, + operation.otherwise.result, + ] case 'ShortCircuit': return [operation.destination, operation.left, operation.right.result] } @@ -1672,10 +1692,6 @@ const operationTypes = (operation: Operation): ReadonlyArray cleanupTypes(entry.cleanup)), ]), ] + case 'Conditional': + return [ + semanticType(operation.type), + ...operation.taken.operations.flatMap(operationTypes), + ...operation.otherwise.operations.flatMap(operationTypes), + ] case 'ShortCircuit': return [semanticType(operation.type), ...operation.right.operations.flatMap(operationTypes)] case 'Move': @@ -1850,6 +1872,8 @@ const accessedOwnerLocals = (operation: Operation): ReadonlyArray => { return [operation.local] case 'Match': return [operation.scrutinee] + case 'Conditional': + return [operation.condition] case 'ShortCircuit': return [operation.left] case 'Literal': @@ -2035,6 +2059,10 @@ const loanViolations = ( process(arm.selected.operations, active) } } + if (operation._tag === 'Conditional') { + process(operation.taken.operations, new Map(active)) + process(operation.otherwise.operations, new Map(active)) + } } for (const [key] of active) { if (!inheritedKeys.has(key) && !globalEndings.has(key)) { @@ -2156,10 +2184,7 @@ const suspensionTypes = (fn: MirFunction): ReadonlyArray => ? [region.completion.outcome] : [ region.completion.outcome, - region.completion.resultType, - region.completion.resultUnion, region.completion.successType, - region.completion.failureType, region.completion.failureValueType, ] const descriptor = region.relay.state @@ -2745,6 +2770,13 @@ export const verify = (self: Module): ReadonlyArray => { >() const localUseCounts = new Map() const successPathOperations = (operation: Operation): ReadonlyArray => { + if (operation._tag === 'Conditional') { + return [ + operation, + ...operation.taken.operations.flatMap(successPathOperations), + ...operation.otherwise.operations.flatMap(successPathOperations), + ] + } if (operation._tag === 'ShortCircuit') { return [operation, ...operation.right.operations.flatMap(successPathOperations)] } @@ -5603,8 +5635,15 @@ export const verify = (self: Module): ReadonlyArray => { rule: 'InvalidNormalization', function: fn.id, region: region.id, - detail: - 'direct static Effect run disagrees with its runner, captures, outcome, or propagation contract', + detail: `direct static Effect run disagrees: ${[ + runnerResultValid ? undefined : 'runner', + outcomeValid ? undefined : 'outcome', + destinationValid ? undefined : 'destination', + parametersValid ? undefined : 'parameters', + propagationValid ? undefined : 'propagation', + ] + .filter((part): part is string => part !== undefined) + .join(', ')}`, }), ) } @@ -5615,30 +5654,11 @@ export const verify = (self: Module): ReadonlyArray => { const destination = fn.localTypes.at(operation.destination.ordinal) const effect = fn.localTypes.at(operation.effect.ordinal) const outcome = fn.localTypes.at(operation.outcome.ordinal) + const success = fn.localTypes.at(operation.successValue.ordinal) + const failure = fn.localTypes.at(operation.failureValue.ordinal) const expectedFailureValue = SilkType.failureValue( SilkType.failureMembers(operation.outcomeType.type), ) - const expectedResult = SilkType.result( - operation.outcomeType.type.success, - expectedFailureValue, - ) - const expectedSuccess = SilkType.resultSuccess(operation.outcomeType.type.success) - const expectedFailure = SilkType.resultFailure(expectedFailureValue) - const selectedSuccess = SilkType.failureCarrierMember( - operation.resultUnion, - operation.successTag, - 'ZeroBased', - ) - const selectedFailure = SilkType.failureCarrierMember( - operation.resultUnion, - operation.failureTag, - 'ZeroBased', - ) - const tagsValid = - selectedSuccess !== undefined && - selectedFailure !== undefined && - SilkType.equals(selectedSuccess, expectedSuccess) && - SilkType.equals(selectedFailure, expectedFailure) const disagreements = [ runner?.result._tag === 'EffectOutcome' && SilkType.equals(runner.result.type, operation.outcomeType.type) @@ -5652,25 +5672,26 @@ export const verify = (self: Module): ReadonlyArray => { SilkType.equals(outcome.type, operation.outcomeType.type) ? undefined : 'outcome', - destination?._tag === 'Nominal' && SilkType.equals(destination.type, expectedResult) + destination?._tag === 'bool' ? undefined : 'destination', + success !== undefined && + SilkType.equals(semanticType(success), operation.outcomeType.type.success) + ? undefined + : 'success', + failure !== undefined && SilkType.equals(semanticType(failure), expectedFailureValue) ? undefined - : 'destination', - SilkType.equals(operation.resultType.type, expectedResult) ? undefined : 'result-type', + : 'failure', SilkType.equals(operation.failureValueType, expectedFailureValue) ? undefined : 'failure-value', - SilkType.equals(operation.successType, expectedSuccess) ? undefined : 'success', - SilkType.equals(operation.failureType, expectedFailure) ? undefined : 'failure', - SilkType.equals(operation.resultShape.type, expectedResult) + SilkType.equals(operation.successShape.type, operation.outcomeType.type.success) ? undefined - : 'result-shape', + : 'success-shape', SilkType.equals(operation.outcomeShape.type, operation.outcomeType.type) ? undefined : 'outcome-shape', SilkType.equals(operation.failureValueShape.type, expectedFailureValue) ? undefined : 'failure-shape', - tagsValid ? undefined : 'tags', ].filter((disagreement): disagreement is string => disagreement !== undefined) if (disagreements.length > 0) { violations.push( @@ -5679,7 +5700,7 @@ export const verify = (self: Module): ReadonlyArray => { rule: 'InvalidEffectOperation', function: fn.id, region: region.id, - detail: `effect result runner, channel data, tags, or calling shapes disagree: ${disagreements.join(', ')}`, + detail: `effect result runner, channel data, or calling shapes disagree: ${disagreements.join(', ')}`, }), ) } diff --git a/packages/compiler/src/NativeEffectOperation.ts b/packages/compiler/src/NativeEffectOperation.ts index 3e64a5486..a15defb23 100644 --- a/packages/compiler/src/NativeEffectOperation.ts +++ b/packages/compiler/src/NativeEffectOperation.ts @@ -840,35 +840,34 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op zero, `effect_result_success${operation.destination.ordinal}`, ) - const successBlock = yield* LlvmBlock.make( - body, - `effect_result${operation.destination.ordinal}_success`, - ) - const failureBlock = yield* LlvmBlock.make( - body, - `effect_result${operation.destination.ordinal}_failure`, - ) - const followingBlock = yield* LlvmBlock.make( - body, - `effect_result${operation.destination.ordinal}_following`, + nativeStorage.locals.set( + operation.destination.ordinal, + Object.freeze([ + yield* FunctionBody.cast( + body, + 'zext', + succeeded, + i32, + `effect_result_success_flag${operation.destination.ordinal}`, + ), + ]), ) - yield* FunctionBody.conditionalBranch(body, succeeded, successBlock, failureBlock) - const destinationLanes = operation.resultShape.lanes - const destinationPayloadLanes = destinationLanes.slice(1) const outcomeLanes = operation.outcomeShape.lanes - const writeBranch = Effect.fnUntraced(function* ( - outerTag: number, + const successLaneCount = + operation.outcomeShape.tree._tag === 'OutcomeShape' + ? operation.outcomeShape.tree.success.laneCount + : 0 + const coerce = Effect.fnUntraced(function* ( values: ReadonlyArray, - lanes: ReadonlyArray, + sourceLanes: ReadonlyArray, + targetLanes: ReadonlyArray, label: string, ) { - const branch: Array = [ - yield* Constant.integerSigned(builder, i32, BigInt(outerTag)), - ] - for (const [ordinal, targetLane] of destinationPayloadLanes.entries()) { + const coerced: Array = [] + for (const [ordinal, targetLane] of targetLanes.entries()) { const input = values.at(ordinal) - const sourceLane = lanes.at(ordinal) - branch.push( + const sourceLane = sourceLanes.at(ordinal) + coerced.push( input === undefined || sourceLane === undefined ? yield* Constant.nullValue(builder, NativeType.laneType(types, targetLane)) : yield* NativeArith.coerceLane( @@ -880,83 +879,45 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op ), ) } - yield* NativeStorage.storeMutable( - nativeStorage, - operation.destination, - Object.freeze(branch), - ) + return Object.freeze(coerced) }) - yield* LlvmBlock.setInsertionPoint(body, successBlock) - const successLaneCount = - operation.outcomeShape.tree._tag === 'OutcomeShape' - ? operation.outcomeShape.tree.success.laneCount - : 0 - yield* writeBranch( - operation.successTag, - Object.freeze(outcomeValues.slice(1, 1 + successLaneCount)), - Object.freeze(outcomeLanes.slice(1, 1 + successLaneCount)), - `effect_result${operation.destination.ordinal}_success`, - ) - yield* FunctionBody.branch(body, followingBlock) - yield* LlvmBlock.setInsertionPoint(body, failureBlock) - if (SilkType.failureMembers(operation.outcomeType.type).length === 0) { - if (trapBlock === undefined) - trapBlock = yield* LlvmBlock.make(body, 'effect_result_invalid_tag') - yield* FunctionBody.branch(body, trapBlock) - } else { - const failureValues: Array = [] - if (SilkType.isUnion(operation.failureValueType)) { - failureValues.push( - yield* FunctionBody.binary( - body, - 'sub', - tag, - yield* Constant.integerSigned(builder, i32, 1n), - `effect_result${operation.destination.ordinal}_failure_tag`, - ), - ) - } - failureValues.push(...outcomeValues.slice(1)) - const failureLanes: Array = [] - if (SilkType.isUnion(operation.failureValueType)) { - const failureTagLane = operation.failureValueShape.lanes.at(0) - if (failureTagLane === undefined) - throw new RangeError('Effect result lost its failure-union tag lane') - failureLanes.push(failureTagLane) - } - failureLanes.push(...outcomeLanes.slice(1)) - yield* writeBranch( - operation.failureTag, - Object.freeze(failureValues), - Object.freeze(failureLanes), - `effect_result${operation.destination.ordinal}_failure`, - ) - yield* FunctionBody.branch(body, followingBlock) - } - yield* LlvmBlock.setInsertionPoint(body, followingBlock) - // Both arms of this outcome dispatch reach here, so neither arm's cached - // values are readable in the join. Reloading re-roots them at this block. - yield* NativeStorage.reloadRoots( - nativeStorage, - `effect_result${operation.destination.ordinal}_following`, + nativeStorage.locals.set( + operation.successValue.ordinal, + yield* coerce( + Object.freeze(outcomeValues.slice(1, 1 + successLaneCount)), + Object.freeze(outcomeLanes.slice(1, 1 + successLaneCount)), + operation.successShape.lanes, + `effect_result${operation.destination.ordinal}_success`, + ), ) - const storage = nativeStorage.mutableStorage.get(operation.destination.ordinal) - if (storage === undefined) - throw new RangeError('Effect result destination is not materialized') - const loaded: Array = [] - for (const [ordinal, pointer] of storage.entries()) { - const lane = destinationLanes.at(ordinal) - if (lane === undefined) throw new RangeError('Effect result destination lost a lane') - loaded.push( - yield* FunctionBody.load( + const failureValues: Array = [] + const failureLanes: Array = [] + if (SilkType.isUnion(operation.failureValueType)) { + failureValues.push( + yield* FunctionBody.binary( body, - NativeType.laneType(types, lane), - pointer, - `effect_result${operation.destination.ordinal}_${ordinal}`, + 'sub', + tag, + yield* Constant.integerSigned(builder, i32, 1n), + `effect_result${operation.destination.ordinal}_failure_tag`, ), ) + const failureTagLane = operation.failureValueShape.lanes.at(0) + if (failureTagLane === undefined) + throw new RangeError('Effect result lost its failure-union tag lane') + failureLanes.push(failureTagLane) } - nativeStorage.locals.set(operation.destination.ordinal, Object.freeze(loaded)) + failureValues.push(...outcomeValues.slice(1)) + failureLanes.push(...outcomeLanes.slice(1)) + nativeStorage.locals.set( + operation.failureValue.ordinal, + yield* coerce( + Object.freeze(failureValues), + Object.freeze(failureLanes), + operation.failureValueShape.lanes, + `effect_result${operation.destination.ordinal}_failure`, + ), + ) break } case 'CloseEffectEntry': { diff --git a/packages/compiler/src/Ownership.ts b/packages/compiler/src/Ownership.ts index e82d51a47..9fa7c49d2 100644 --- a/packages/compiler/src/Ownership.ts +++ b/packages/compiler/src/Ownership.ts @@ -1014,9 +1014,7 @@ const checkExpression = ( return } case 'EffectResult': - // The intrinsic consumes its protected Effect exactly like its source-callable contract. - // Visiting the dedicated HIR operand prevents the runner's exit cleanup from releasing an - // affine environment that the reification operation already transferred to its runner. + // The intrinsic consumes all three operands exactly like its source-callable contract. checkExpression( state, live, @@ -1025,6 +1023,22 @@ const checkExpression = ( guard, escaping, ) + checkExpression( + state, + live, + expression.success, + argumentConsumes(expression.success), + guard, + escaping, + ) + checkExpression( + state, + live, + expression.failure, + argumentConsumes(expression.failure), + guard, + escaping, + ) return case 'EffectCatch': // The sealed primitive has the same owned operands as its ordinary callable contract. @@ -1402,7 +1416,12 @@ const analyzeLoans = ( return Object.freeze( expression.elements.flatMap((element) => movedExecutableBindings(element.expression)), ) - if (expression._tag === 'EffectResult') return movedExecutableBindings(expression.protected) + if (expression._tag === 'EffectResult') + return Object.freeze([ + ...movedExecutableBindings(expression.protected), + ...movedExecutableBindings(expression.success), + ...movedExecutableBindings(expression.failure), + ]) if (expression._tag === 'EffectCatch') return Object.freeze([ ...movedExecutableBindings(expression.protected), @@ -1532,6 +1551,8 @@ const analyzeLoans = ( return case 'EffectResult': scanRunEnds(expression.protected, region) + scanRunEnds(expression.success, region) + scanRunEnds(expression.failure, region) return case 'EffectBindRequirement': scanRunEnds(expression.protected, region) diff --git a/packages/compiler/src/ProvisionalMir.ts b/packages/compiler/src/ProvisionalMir.ts index fe60a063e..9d5f323ff 100644 --- a/packages/compiler/src/ProvisionalMir.ts +++ b/packages/compiler/src/ProvisionalMir.ts @@ -676,61 +676,21 @@ const runnerOf = ( const reifyPolicy = ( outcome: Type.Effect, - result: Type.Type, context: BuildContext, ): Extract | undefined => { const failureValueType = Type.failureValue(Type.failureMembers(outcome)) - const successType = Type.resultSuccess(outcome.success) - const failureType = Type.resultFailure(failureValueType) - const union = Type.union([successType, failureType]) - if (union._tag !== 'Normalized' || !Type.isUnion(union.type)) return undefined - if (!Type.isNominal(result)) return undefined - const resultType = result - const resultEntry = Layout.entry(context.layout, resultType) - const successEntry = Layout.entry(context.layout, successType) - const failureEntry = Layout.entry(context.layout, failureType) - const resultField = - resultEntry?.representation._tag === 'Aggregate' - ? resultEntry.representation.fields.at(0)?.id - : undefined - const successField = - successEntry?.representation._tag === 'Aggregate' - ? successEntry.representation.fields.at(0)?.id - : undefined - const failureField = - failureEntry?.representation._tag === 'Aggregate' - ? failureEntry.representation.fields.at(0)?.id - : undefined - const resultShape = Layout.callingShape(context.layout, resultType) + const successType = outcome.success const outcomeShape = Layout.callingShape(context.layout, outcome) + const successShape = Layout.callingShape(context.layout, successType) const failureValueShape = Layout.callingShape(context.layout, failureValueType) - const successTag = union.type.members.findIndex((member) => Type.equals(member, successType)) - const failureTag = union.type.members.findIndex((member) => Type.equals(member, failureType)) - if ( - resultField === undefined || - successField === undefined || - failureField === undefined || - resultShape === undefined || - outcomeShape === undefined || - failureValueShape === undefined || - successTag < 0 || - failureTag < 0 - ) + if (outcomeShape === undefined || successShape === undefined || failureValueShape === undefined) return undefined return Object.freeze({ _tag: 'Reify', outcome, - resultType, - resultField, - resultUnion: union.type, successType, - successField, - successTag, - failureType, - failureField, - failureTag, failureValueType, - resultShape, + successShape, outcomeShape, failureValueShape, }) @@ -823,11 +783,7 @@ const controlsOfCatch = ( if (!Type.isEffect(protectedEffect) || !Type.isEffect(resultEffect)) return Object.freeze([]) const protectedRunner = runnerOf(expression.protected, context) - const protectedResult = Type.result( - protectedEffect.success, - Type.failureValue(Type.failureMembers(protectedEffect)), - ) - const protectedPolicy = reifyPolicy(protectedRunner.outcome, protectedResult, context) + const protectedPolicy = reifyPolicy(protectedRunner.outcome, context) if (protectedRunner.classification !== 'Synchronous' && protectedPolicy !== undefined) { const id = controlId(execution, expression.span, 0, 'Invoke') const complete = controlId(execution, expression.span, 0, 'Complete') @@ -964,11 +920,7 @@ const controlsOf = ( if (runner.classification !== 'Synchronous') { const policy = expression.subject._tag === 'EffectResult' - ? reifyPolicy( - runner.outcome, - Type.substitute(expression.type, context.instance.substitution), - context, - ) + ? reifyPolicy(runner.outcome, context) : Object.freeze({ _tag: 'Propagate' as const, outcome: runner.outcome, diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index d8aec00ef..304f9b347 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -1088,6 +1088,8 @@ const collectExpression = ( case 'EffectResult': collectIntrinsicReference(expression.reference, index, pending) collectExpression(expression.protected, index, scope, pending) + collectExpression(expression.success, index, scope, pending) + collectExpression(expression.failure, index, scope, pending) return case 'EffectCatch': collectIntrinsicReference(expression.reference, index, pending) diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index 43b0fb528..daa3a5819 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -96,7 +96,7 @@ export const modules = [ module: 'silk/effect', path: 'silk/effect.silk', sourceIdentity: 'silk/effect', - digest: '9740e2cba0147417dac21f4e896fbc33d3413f47ebef4f7106258a53019bdb46', + digest: '1960eaa39497fa28a083d29ee7b5c4dacc74df2e7819a666d79c30e114721e5b', documentation: 'silk/effect.silk', layer: 'portable', runtimeInventory: [ @@ -109,7 +109,7 @@ export const modules = [ ], namespace: 'Effect', source: - "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n return run Intrinsic.effectResult(move protected)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run onFailure(move error)\n }\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", + "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n return run Intrinsic.effectResult>(move protected, succeed, failResult)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run onFailure(move error)\n }\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", }, { module: 'silk/execution', @@ -256,10 +256,10 @@ export const modules = [ module: 'silk/filesystem', path: 'silk/filesystem.silk', sourceIdentity: 'silk/filesystem', - digest: '371a54137c198d30d24760811a42596e887a055805160213b6f8166fa1c70c86', + digest: 'b533b1c131e6829dc2be296bf4854a90c1df94f158bae237e584c909609b7668', documentation: 'silk/filesystem.silk', layer: 'portable', - runtimeInventory: ['effectResult', 'replace', 'stringFromUtf8Unchecked'], + runtimeInventory: ['replace', 'stringFromUtf8Unchecked'], namespace: 'FileSystem', aliases: [ 'DirectoryEntry', @@ -272,7 +272,7 @@ export const modules = [ 'Path', ], source: - '//! Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.\n//!\n//! # When to use\n//! Build provider-absolute [`Path`] values with [`make`] or [`fromBytes`], then run operations\n//! through a supplied [`FileSystem`]. Use [`rawBytes`] for platform values that must round-trip even\n//! when they are not UTF-8, and [`resolve`] for lexical relative-path resolution.\n//!\n//! # Details\n//! Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and\n//! embedded `.` or `..`. Resolution handles relative dot components but rejects escape above root.\n//! Directory listings return independently owned child paths in deterministic path-byte order.\n//! Portable [`FileError`] data names both the operation and a closed recovery reason, with an\n//! optional provider code for diagnostics.\n//!\n//! Temporary directories have an explicit lifecycle because removal can fail and needs services.\n//! Use [`release`] when cleanup failure matters, or [`releaseIgnored`] as an infallible finalizer\n//! only after deliberately accepting that loss.\n//!\n//! # Gotchas\n//! A path created from arbitrary bytes may not have a valid text view. Keep using [`rawBytes`] unless\n//! the bytes were validated as UTF-8; [`view`] and [`name`] rely on that caller knowledge.\n//!\n//! # Examples\n//! ## Construct and inspect a portable path\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as FileSystem\n//!\n//! effect fn example() -> i32\n//! ! FileSystem.FileError | Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let path = run FileSystem.make("/workspace")\n//! |> Effect.provideMut(&mut allocator)\n//! if FileSystem.name(&path) == "workspace" {\n//! return 42\n//! }\n//! return 0\n//! }\n//!\n//! effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(example(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n append as bytesAppend,\n asSlice as bytesAsSlice\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string {\n InvalidUtf8,\n fromUtf8 as stringFromUtf8,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n length as vectorLength,\n make as vectorMake,\n pop as vectorPop\n}\n\n/// An owned, normalized absolute path in a [`FileSystem`] provider\'s portable namespace.\n///\n/// # Details\n///\n/// Portable `/` means the selected provider\'s root, not necessarily the host operating system\'s\n/// root. Construct paths through [`make`], [`fromBytes`], [`root`], [`join`], or [`resolve`]; the\n/// representation is private so every `Path` satisfies the normalization rules.\npub struct Path {\n bytes: Bytes\n nameBytes: Bytes\n}\n\n/// Minimal portable metadata for one regular file.\npub struct FileInfo {\n /// Complete file length in bytes.\n pub byteLength: usize\n}\n\n/// Portable metadata identifying a directory; no platform-specific fields are exposed.\npub struct DirectoryInfo {}\n\n/// The closed portable kind of one directory entry.\npub struct DirectoryEntryKind {\n /// Stable portable kind code selected by [`file`] or [`directory`].\n pub code: i32\n}\n\n/// One immediate directory child with an independently owned complete [`Path`].\npub struct DirectoryEntry {\n /// Independently owned complete path to the child.\n pub path: Path\n /// Portable kind reported for the child.\n pub kind: DirectoryEntryKind\n}\n\n/// The stable portable operation category stored in a [`FileError`].\npub struct FileOperation {\n /// Stable code identifying the attempted portable operation.\n pub code: i32\n}\n\n/// A stable portable recovery category stored in a [`FileError`].\npub struct FileReason {\n /// Stable code identifying the portable recovery reason.\n pub code: i32\n}\n\n/// An allocation-free portable failure naming the attempted operation and recovery reason.\n///\n/// # Details\n///\n/// Match or compare [`operationCode`] and [`reasonCode`] for portable recovery. [`providerCode`]\n/// may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions\n/// must not depend on it.\npub struct FileError {\n /// The operation that failed.\n pub operation: FileOperation\n /// The portable reason callers can recover by.\n pub reason: FileReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Constructs the regular-file [`DirectoryEntryKind`].\npub fn file() -> DirectoryEntryKind { return DirectoryEntryKind { code: 0 } }\n\n/// Constructs the directory [`DirectoryEntryKind`].\npub fn directory() -> DirectoryEntryKind { return DirectoryEntryKind { code: 1 } }\n\n/// Returns the stable code for a consumed [`DirectoryEntryKind`]: `0` for file, `1` for directory.\npub fn entryKindCode(kind: DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Reads the stable directory-entry kind code through a borrow.\nfn borrowedKindCode(kind: &DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Constructs regular-file metadata with the complete length in bytes.\npub fn fileInfo(byteLength: usize) -> FileInfo {\n return FileInfo { byteLength: byteLength }\n}\n\n/// Constructs the fieldless portable directory metadata value.\npub fn directoryInfo() -> DirectoryInfo { return DirectoryInfo {} }\n\n/// Constructs a directory entry by taking ownership of its complete child `path` and `kind`.\npub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntry {\n return DirectoryEntry { path: move path, kind: move kind }\n}\n\n/// Selects the read-file operation.\npub fn readFileOperation() -> FileOperation { return FileOperation { code: 0 } }\n\n/// Selects the write-file operation.\npub fn writeFileOperation() -> FileOperation { return FileOperation { code: 1 } }\n\n/// Selects the stat operation.\npub fn statOperation() -> FileOperation { return FileOperation { code: 2 } }\n\n/// Selects the list-directory operation.\npub fn listDirectoryOperation() -> FileOperation { return FileOperation { code: 3 } }\n\n/// Selects the create-directory operation.\npub fn createDirectoryOperation() -> FileOperation { return FileOperation { code: 4 } }\n\n/// Selects the remove-file operation.\npub fn removeFileOperation() -> FileOperation { return FileOperation { code: 5 } }\n\n/// Selects the remove-directory operation.\npub fn removeDirectoryOperation() -> FileOperation { return FileOperation { code: 6 } }\n\n/// Selects path construction and resolution.\npub fn pathOperation() -> FileOperation { return FileOperation { code: 7 } }\n\n/// Selects the create-temporary-directory operation.\npub fn createTemporaryDirectoryOperation() -> FileOperation { return FileOperation { code: 8 } }\n\n/// Returns the stable numeric code of a consumed [`FileOperation`].\npub fn operationCode(operation: FileOperation) -> i32 { return operation.code }\n\n/// Constructs the `NotFound` recovery reason.\npub fn notFound() -> FileReason { return FileReason { code: 0 } }\n\n/// Constructs the `AlreadyExists` recovery reason.\npub fn alreadyExists() -> FileReason { return FileReason { code: 1 } }\n\n/// Constructs the `PermissionDenied` recovery reason.\npub fn permissionDenied() -> FileReason { return FileReason { code: 2 } }\n\n/// Constructs the `InvalidPath` recovery reason.\npub fn invalidPath() -> FileReason { return FileReason { code: 3 } }\n\n/// Constructs the `WrongType` recovery reason.\npub fn wrongType() -> FileReason { return FileReason { code: 4 } }\n\n/// Constructs the `NotEmpty` recovery reason.\npub fn notEmpty() -> FileReason { return FileReason { code: 5 } }\n\n/// Constructs the `NoSpace` recovery reason.\npub fn noSpace() -> FileReason { return FileReason { code: 6 } }\n\n/// Constructs the `TooLarge` recovery reason.\npub fn tooLarge() -> FileReason { return FileReason { code: 7 } }\n\n/// Constructs the `Unsupported` recovery reason.\npub fn unsupported() -> FileReason { return FileReason { code: 8 } }\n\n/// Constructs the catch-all `Other` recovery reason.\npub fn other() -> FileReason { return FileReason { code: 9 } }\n\n/// Returns the stable numeric code of a consumed [`FileReason`].\npub fn reasonCode(reason: FileReason) -> i32 { return reason.code }\n\n/// Constructs a portable [`FileError`] without a provider-specific numeric detail.\npub fn error(operation: FileOperation, reason: FileReason) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false,\n }\n}\n\n/// Constructs a portable [`FileError`] while retaining one provider-specific diagnostic code.\n///\n/// # Details\n///\n/// The numeric `code` is opaque outside that provider. The portable `operation` and `reason` remain\n/// the fields callers should use for recovery.\npub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true,\n }\n}\n\n/// Borrows an error and returns its provider-specific numeric detail, if one was retained.\npub fn providerCode(error: &FileError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\neffect fn raise(error: FileError) -> never ! FileError { fail move error }\n\neffect fn rejectPath() -> never ! FileError {\n fail error(pathOperation(), invalidPath())\n}\n\nfn byte(value: u8) -> i32 { return u8.toI32(value) }\n\nfn containsNul(values: &[u8]) -> bool {\n let mut index = usize.ZERO\n while index < values.length {\n if values[index] == u8.toU8(0) { return true }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validUtf8(values: &[u8]) -> bool {\n let decoded = stringFromUtf8(values)\n return match move decoded {\n Result.Success { value: text } => true\n Result.Failure { error: invalid } => false\n }\n}\n\nfn isDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != usize.ONE { return false }\n return byte(values[start]) == 46\n}\n\nfn isDotDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != 2 { return false }\n if byte(values[start]) != 46 { return false }\n return byte(values[start + usize.ONE]) == 46\n}\n\nfn validAbsolute(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) != 47 { return false }\n if containsNul(values) { return false }\n if values.length == usize.ONE { return true }\n let mut start = usize.ONE\n let mut index = usize.ONE\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validRelativeFragment(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) == 47 { return false }\n if containsNul(values) { return false }\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\neffect fn appendRange(\n target: Bytes,\n source: &[u8],\n start: usize,\n end: usize\n) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = move target\n let mut index = start\n while index < end {\n let one = [source[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\nfn finalNameStart(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ONE }\n let mut index = values.length\n while usize.ZERO < index {\n index = index - usize.ONE\n if byte(values[index]) == 47 { return index + usize.ONE }\n }\n return usize.ZERO\n}\n\neffect fn finishPath(bytes: Bytes) -> Path ! OutOfMemoryError ? &mut Allocator {\n let view = bytesAsSlice(&bytes)\n let start = finalNameStart(view)\n let nameBytes = run appendRange(bytesMake(), view, start, view.length)\n return Path { bytes: move bytes, nameBytes: move nameBytes }\n}\n\n/// Copies UTF-8 text into an owned, normalized provider-absolute [`Path`].\n///\n/// # Details\n///\n/// The text must begin with `/`. Root is valid; every other path must have nonempty components and\n/// no trailing slash, NUL, `.` component, or `..` component. Invalid input fails with\n/// `FileError(pathOperation(), invalidPath())`; copying can fail with [`OutOfMemoryError`].\npub effect fn make(value: string) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let values = stringUtf8Bytes(value)\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Constructs an owned normalized provider-absolute Path from exact platform bytes.\n///\n/// # Details\n///\n/// Platform paths are byte sequences, and a caller that received one from the platform — a\n/// directory entry, an argument, an environment value — must be able to hand it back unchanged.\n/// The same normalization applies as for textual construction: the value is absolute, rejects NUL,\n/// and rejects `.`, `..`, empty components, and trailing separators. Well-formed text is not\n/// required, so a Path built this way may have no `string` view.\npub effect fn fromBytes(values: &[u8]) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Allocates the portable root path `/` in the selected allocator.\npub effect fn root() -> Path ! OutOfMemoryError ? &mut Allocator {\n let mut copied = bytesMake()\n let appended = run bytesAppend(&mut copied, stringUtf8Bytes("/"))\n return run finishPath(move copied)\n}\n\nfn pathBytes(self: &Path) -> &[u8] { return bytesAsSlice(&self.bytes) }\n\n/// Borrows the complete normalized path as exact platform bytes.\n///\n/// # Details\n///\n/// This is the lossless view. It round-trips a Path built from platform bytes even when those\n/// bytes are not well-formed text, which the `string` view cannot promise.\npub fn rawBytes(self: &Path) -> &[u8] {\n return pathBytes(self)\n}\n\n/// Borrows the complete path as text when its bytes are known to be valid UTF-8.\n///\n/// # Details\n///\n/// Paths from [`make`], [`join`], [`joinUtf8`], and [`resolve`] satisfy that precondition. A path\n/// created with [`fromBytes`] may not; use [`rawBytes`] unless the source bytes were validated.\npub fn view(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(pathBytes(self)) }\n return ""\n}\n\n/// Returns `true` exactly when this path is the portable root `/`.\npub fn isRoot(self: &Path) -> bool { return bytesAsSlice(&self.bytes).length == usize.ONE }\n\n/// Borrows the final component as text; root returns empty text.\n///\n/// # Details\n///\n/// This has the same UTF-8 precondition as [`view`]. It does not allocate or include a separator.\npub fn name(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(bytesAsSlice(&self.nameBytes)) }\n return ""\n}\n\neffect fn joinBytes(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validRelativeFragment(fragment) == false { return run rejectPath() }\n let baseBytes = pathBytes(base)\n let mut combined = run appendRange(bytesMake(), baseBytes, usize.ZERO, baseBytes.length)\n if isRoot(base) == false {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeChild = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeChild, fragment, usize.ZERO, fragment.length)\n return run finishPath(move combined)\n}\n\n/// Appends one normalized relative text fragment to an absolute base path.\n///\n/// # Details\n///\n/// `fragment` must be nonempty and relative, with no NUL, empty, `.`, or `..` component and no\n/// trailing slash. Use [`resolve`] when dot components should be interpreted instead of rejected.\npub effect fn join(\n base: &Path,\n fragment: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return run joinBytes(base, stringUtf8Bytes(fragment))\n}\n\n/// Validates UTF-8 bytes as one normalized relative fragment and appends them to `base`.\n///\n/// # Details\n///\n/// This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the\n/// same malformed components rejected by [`join`] fail with the `InvalidPath` reason.\npub effect fn joinUtf8(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validUtf8(fragment) == false { return run rejectPath() }\n return run joinBytes(base, fragment)\n}\n\nfn componentCount(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ZERO }\n let mut count = usize.ONE\n let mut index = usize.ONE\n while index < values.length {\n if byte(values[index]) == 47 { count = count + usize.ONE }\n index = index + usize.ONE\n }\n return count\n}\n\nfn survivingRelative(values: &[u8], after: usize) -> bool {\n let mut depth = usize.ONE\n let mut start = after\n let mut index = after\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDotDot(values, start, index) {\n depth = depth - usize.ONE\n if depth == usize.ZERO { return false }\n } else {\n if isDot(values, start, index) == false { depth = depth + usize.ONE }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return true\n}\n\n/// Resolves relative text lexically against an explicit absolute base.\n///\n/// # Details\n///\n/// Empty text and `.` keep the base; `..` removes components; ordinary components append. An\n/// absolute relative value, an empty interior component, NUL, or any attempt to escape above root\n/// fails with the `InvalidPath` reason. Resolution is lexical and never accesses the filesystem.\npub effect fn resolve(\n base: &Path,\n relativeText: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let relative = stringUtf8Bytes(relativeText)\n if containsNul(relative) { return run rejectPath() }\n if usize.ZERO < relative.length {\n if byte(relative[usize.ZERO]) == 47 { return run rejectPath() }\n }\n let baseBytes = pathBytes(base)\n let mut keptBase = componentCount(baseBytes)\n let mut relativeDepth = usize.ZERO\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start == index {\n if index != relative.length { return run rejectPath() }\n } else {\n if isDotDot(relative, start, index) {\n if usize.ZERO < relativeDepth {\n relativeDepth = relativeDepth - usize.ONE\n } else {\n if keptBase == usize.ZERO { return run rejectPath() }\n keptBase = keptBase - usize.ONE\n }\n } else {\n if isDot(relative, start, index) == false {\n relativeDepth = relativeDepth + usize.ONE\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n let mut combined = bytesMake()\n let rooted = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n let mut included = usize.ZERO\n start = usize.ONE\n index = usize.ONE\n while index <= baseBytes.length {\n let mut boundary = false\n if index == baseBytes.length {\n boundary = true\n } else {\n if byte(baseBytes[index]) == 47 { boundary = true }\n }\n if boundary {\n if included < keptBase {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeBaseComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeBaseComponent, baseBytes, start, index)\n included = included + usize.ONE\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n start = usize.ZERO\n index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDot(relative, start, index) == false {\n if isDotDot(relative, start, index) == false {\n if survivingRelative(relative, index + usize.ONE) {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeRelativeComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeRelativeComponent, relative, start, index)\n included = included + usize.ONE\n }\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return run finishPath(move combined)\n}\n\n/// Allocates an independently owned parent path, or [`None`] when `self` is root.\n///\n/// # Details\n///\n/// The result does not borrow `self`. A direct child of root has root as its parent.\npub effect fn parent(\n self: &Path\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n if isRoot(self) { return none() }\n let values = pathBytes(self)\n let nameStart = finalNameStart(values)\n let mut end = usize.ONE\n if nameStart != usize.ONE { end = nameStart - usize.ONE }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, end)\n let owned = run finishPath(move copied)\n return some(move owned)\n}\n\n/// Portable mutable service for normalized paths and whole-file operations.\n///\n/// # Details\n///\n/// Application code supplies one provider lexically with `Effect.provideMut`; tests can implement\n/// this service in memory, while native applications can use `silk.os_filesystem`. The service owns\n/// platform policy, but every implementation must preserve the portable error categories,\n/// create-or-truncate writes, and deterministic listing order described here.\n///\n/// # Examples\n/// ## Write a file after creating its parents\n/// ```silk\n/// import silk.allocator { Allocator }\n///\n/// import silk.filesystem as FileSystem\n///\n/// import silk.usize as usize\n///\n/// pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize\n/// ! FileSystem.FileError | Allocator.OutOfMemoryError\n/// ? &mut FileSystem.FileSystem | &mut Allocator {\n/// let written = run FileSystem.writeFileWithParents(path, contents)\n/// return contents.length\n/// }\n/// ```\npub service FileSystem {\n /// Reads one complete regular file into independently owned bytes.\n ///\n /// # Details\n ///\n /// Reading a directory fails with `WrongType`. Allocation of the returned [`Bytes`] may fail\n /// independently of the provider read.\n effect fn readFile(\n path: &Path\n ) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Writes one complete byte view with create-or-truncate semantics.\n ///\n /// # Details\n ///\n /// A missing file is created; an existing regular file is replaced by exactly `bytes`. The call\n /// does not create missing parent directories—use [`writeFileWithParents`] for that workflow.\n effect fn writeFile(path: &Path, bytes: &[u8]) -> () ! FileError ? &mut FileSystem\n /// Returns [`FileInfo`] or [`DirectoryInfo`] for the path without opening file contents.\n ///\n /// # Details\n ///\n /// Missing paths fail with `NotFound`; providers use `WrongType` only when an operation requires a\n /// particular kind, not for this discriminating query.\n effect fn stat(path: &Path) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem\n /// Returns immediate owned children in deterministic complete-path byte order.\n ///\n /// # Details\n ///\n /// The result is not recursive. Each `DirectoryEntry.path` is independently owned and may be\n /// retained after the listing vector is released.\n effect fn listDirectory(\n path: &Path\n ) -> Vector ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Creates exactly one missing directory whose parent already exists.\n ///\n /// # Details\n ///\n /// Existing paths fail with `AlreadyExists`; use [`createDirectoriesRecursively`] to ensure every\n /// missing component.\n effect fn createDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one regular file and fails with `WrongType` for a directory.\n effect fn removeFile(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one empty directory.\n ///\n /// # Details\n ///\n /// A nonempty directory fails with `NotEmpty`; use [`removeDirectoryRecursively`] only when all\n /// descendants are intentionally in scope for removal.\n effect fn removeDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Creates one directory under an existing parent under a name no other caller holds.\n ///\n /// # Details\n ///\n /// The provider chooses the name\'s unique part and returns the complete Path, because only the\n /// provider can create and claim a name in one step. A caller that supplied the name would have\n /// to check-then-create, and the gap between those two is exactly the race this avoids.\n /// `prefix` is a byte prefix for the provider-chosen child name, not a complete path. The returned\n /// directory already exists and is an immediate child of `parent`.\n effect fn createTemporaryDirectory(\n parent: &Path,\n prefix: &[u8]\n ) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n}\n\n/// A directory a caller owns outright, together with everything written inside it.\n///\n/// # Details\n///\n/// Ownership is affine: `TemporaryDirectory` holds an owned `Path`, so exactly one binding holds\n/// it and the compiler rejects a second use of a moved one. Ownership is not, however, a `Drop`\n/// hook. Removing a directory is a fallible operation that requires the `FileSystem` capability,\n/// and a `Drop` hook may carry neither a failure row nor a requirement row, so a hook here could\n/// only be written by inventing an infallible intrinsic over a fallible syscall. Release is\n/// therefore explicit and honest about both rows — see `release`.\n///\n/// Scope ownership comes from composition rather than from a hook: `Effect.ensuring(release)`\n/// runs the release whatever the protected Effect\'s outcome. Because `ensuring` types its\n/// finalizer `! never`, that composition has to say what a failed removal means; `releaseIgnored`\n/// is the stdlib\'s answer and names the loss at the call site.\npub struct TemporaryDirectory {\n /// The complete owned path callers use while the scope remains live.\n pub path: Path\n}\n\n/// Creates an explicitly owned temporary directory under `parent` with a name beginning in `prefix`.\n///\n/// # Details\n///\n/// The result is owned. Nothing removes it until a caller runs [`release`] or [`releaseIgnored`].\n/// The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in\n/// one operation.\npub effect fn temporaryDirectory(\n parent: &Path,\n prefix: string\n) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let created = run FileSystem.createTemporaryDirectory(parent, stringUtf8Bytes(prefix))\n return TemporaryDirectory { path: move created }\n}\n\n/// Consumes one TemporaryDirectory and removes it together with everything inside it.\n///\n/// # Details\n///\n/// Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the\n/// tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a\n/// failed cleanup uses this operation and handles the failure. The owner is consumed even when\n/// removal fails, so copy any diagnostic path information needed before calling.\npub effect fn release(\n self: TemporaryDirectory\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let owned = move self\n let removed = run removeDirectoryRecursively(&owned.path)\n drop owned\n return ()\n}\n\neffect fn discardReleaseFailure(error: FileError | OutOfMemoryError) -> () { return () }\n\n/// Consumes one TemporaryDirectory, removes it, and discards a failed removal.\n///\n/// # Details\n///\n/// This exists because `Effect.ensuring` types its finalizer `! never`, so a fallible release has\n/// to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a\n/// caller reading `releaseIgnored` at the call site can see that a failed removal is being\n/// dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —\n/// a directory the host will reap — and what is kept is the protected Effect\'s own outcome, which\n/// is the answer the program was computing.\n///\n/// A caller who needs the failure uses `release` instead and does not compose it with `ensuring`.\n///\n/// The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer\n/// starts. Derive the required paths before you give the owner to the finalizer.\npub effect fn releaseIgnored(\n self: TemporaryDirectory\n) -> () ? &mut FileSystem | &mut Allocator {\n return run Effect.catchAll(release(move self), discardReleaseFailure)\n}\n\n/// Copies one recorded Path out of the walk\'s own record.\n///\n/// The walk appends to the same record it is reading, so it reads through a copy rather than\n/// through a borrow that the next append would invalidate.\neffect fn recordedCopy(\n recorded: &Vector,\n index: usize\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return match &vectorAsSlice(recorded)[index] {\n Path { bytes, nameBytes } => run fromBytes(bytesAsSlice(&bytes))\n }\n}\n\n/// Removes a directory, every descendant file, and every descendant directory.\n///\n/// # Details\n///\n/// Two passes, because the portable primitive removes exactly one *empty* directory. The first\n/// pass walks the tree front to back, unlinking every file it meets and recording every directory\n/// it meets; the second removes the recorded directories back to front. That order is\n/// child-before-parent for free: a directory is always recorded before the children found inside\n/// it, so reversing the record reverses the containment. Neither pass recurses, so depth costs\n/// vector capacity rather than stack.\n///\n/// This operation is destructive and not transactional. If a provider or allocation failure occurs,\n/// removals already completed remain completed and the remaining tree is left in place.\npub effect fn removeDirectoryRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let mut recorded = vectorMake()\n let seed = run fromBytes(rawBytes(path))\n let noted = run vectorAppend(&mut recorded, move seed)\n let mut index = usize.ZERO\n while index < vectorLength(&recorded) {\n let current = run recordedCopy(&recorded, index)\n let entries = run FileSystem.listDirectory(¤t)\n let listed = vectorAsSlice(&entries)\n let mut cursor = usize.ZERO\n while cursor < listed.length {\n let childKind = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } => borrowedKindCode(&childEntryKind)\n }\n if childKind == 0 {\n let unlinked = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run FileSystem.removeFile(&childPath)\n }\n } else {\n let toRemove = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run fromBytes(rawBytes(&childPath))\n }\n let notedChild = run vectorAppend(&mut recorded, move toRemove)\n }\n cursor = cursor + usize.ONE\n }\n index = index + usize.ONE\n }\n while usize.ZERO < vectorLength(&recorded) {\n let taken = vectorPop(&mut recorded)\n let emptied = match move taken {\n Option.Some { value: selected } => move selected\n Option.None => run fromBytes(rawBytes(path))\n }\n let removed = run FileSystem.removeDirectory(&emptied)\n }\n return ()\n}\n\nstruct DirectoryPresent {}\nstruct DirectoryMissing {}\nstruct DirectoryWrongType {}\nstruct DirectoryStatFailure { error: FileError }\n\nfn classifyStatFailure(\n failure: FileError\n) -> DirectoryMissing | DirectoryStatFailure {\n if failure.reason.code == 0 { return DirectoryMissing {} }\n return DirectoryStatFailure { error: move failure }\n}\n\nfn classifyDirectory(\n outcome: Result\n) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure {\n return match move outcome {\n Result.Success { value: info } => match move info {\n DirectoryInfo {} => DirectoryPresent {}\n FileInfo { byteLength } => DirectoryWrongType {}\n }\n Result.Failure { error: failure } => classifyStatFailure(move failure)\n }\n}\n\n/// Ensures that `path` and every missing ancestor exist as directories.\n///\n/// # Details\n///\n/// Existing directories are kept. An existing regular file at any component fails with\n/// `WrongType`; failures other than `NotFound` propagate. This is ordinary stat-then-create\n/// composition, so concurrent namespace changes may still race according to provider policy.\npub effect fn createDirectoriesRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let values = pathBytes(path)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return ()\n}\n\n/// Ensures every parent directory exists, then writes the complete byte view to `path`.\n///\n/// # Details\n///\n/// The final write uses `FileSystem.writeFile` create-or-truncate semantics. Passing root delegates\n/// directly to the provider and normally fails with `WrongType`. Directory creation and writing are\n/// not transactional, so a later failure may leave newly created parents behind.\npub effect fn writeFileWithParents(\n path: &Path,\n bytes: &[u8]\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n if isRoot(path) { return run FileSystem.writeFile(path, bytes) }\n let pathValues = pathBytes(path)\n let nameStart = finalNameStart(pathValues)\n let mut parentEnd = usize.ONE\n if nameStart != usize.ONE { parentEnd = nameStart - usize.ONE }\n let parentBytes = run appendRange(bytesMake(), pathValues, usize.ZERO, parentEnd)\n let ownedParent = run finishPath(move parentBytes)\n let values = pathBytes(&ownedParent)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return run FileSystem.writeFile(path, bytes)\n}\n\neffect fn existsFailure(failure: FileError) -> bool ! FileError {\n if failure.reason.code == 0 { return false }\n return run raise(move failure)\n}\n\n/// Returns whether a file or directory exists at `path`.\n///\n/// # Details\n///\n/// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider\n/// failure propagate so callers cannot mistake an inaccessible path for an absent one.\npub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem {\n let completed = run Intrinsic.effectResult(FileSystem.stat(path))\n return match move completed {\n Result.Success { value: info } => true\n Result.Failure { error: failure } => run existsFailure(move failure)\n }\n}\n', + '//! Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.\n//!\n//! # When to use\n//! Build provider-absolute [`Path`] values with [`make`] or [`fromBytes`], then run operations\n//! through a supplied [`FileSystem`]. Use [`rawBytes`] for platform values that must round-trip even\n//! when they are not UTF-8, and [`resolve`] for lexical relative-path resolution.\n//!\n//! # Details\n//! Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and\n//! embedded `.` or `..`. Resolution handles relative dot components but rejects escape above root.\n//! Directory listings return independently owned child paths in deterministic path-byte order.\n//! Portable [`FileError`] data names both the operation and a closed recovery reason, with an\n//! optional provider code for diagnostics.\n//!\n//! Temporary directories have an explicit lifecycle because removal can fail and needs services.\n//! Use [`release`] when cleanup failure matters, or [`releaseIgnored`] as an infallible finalizer\n//! only after deliberately accepting that loss.\n//!\n//! # Gotchas\n//! A path created from arbitrary bytes may not have a valid text view. Keep using [`rawBytes`] unless\n//! the bytes were validated as UTF-8; [`view`] and [`name`] rely on that caller knowledge.\n//!\n//! # Examples\n//! ## Construct and inspect a portable path\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as FileSystem\n//!\n//! effect fn example() -> i32\n//! ! FileSystem.FileError | Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let path = run FileSystem.make("/workspace")\n//! |> Effect.provideMut(&mut allocator)\n//! if FileSystem.name(&path) == "workspace" {\n//! return 42\n//! }\n//! return 0\n//! }\n//!\n//! effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(example(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n append as bytesAppend,\n asSlice as bytesAsSlice\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string {\n InvalidUtf8,\n fromUtf8 as stringFromUtf8,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n length as vectorLength,\n make as vectorMake,\n pop as vectorPop\n}\n\n/// An owned, normalized absolute path in a [`FileSystem`] provider\'s portable namespace.\n///\n/// # Details\n///\n/// Portable `/` means the selected provider\'s root, not necessarily the host operating system\'s\n/// root. Construct paths through [`make`], [`fromBytes`], [`root`], [`join`], or [`resolve`]; the\n/// representation is private so every `Path` satisfies the normalization rules.\npub struct Path {\n bytes: Bytes\n nameBytes: Bytes\n}\n\n/// Minimal portable metadata for one regular file.\npub struct FileInfo {\n /// Complete file length in bytes.\n pub byteLength: usize\n}\n\n/// Portable metadata identifying a directory; no platform-specific fields are exposed.\npub struct DirectoryInfo {}\n\n/// The closed portable kind of one directory entry.\npub struct DirectoryEntryKind {\n /// Stable portable kind code selected by [`file`] or [`directory`].\n pub code: i32\n}\n\n/// One immediate directory child with an independently owned complete [`Path`].\npub struct DirectoryEntry {\n /// Independently owned complete path to the child.\n pub path: Path\n /// Portable kind reported for the child.\n pub kind: DirectoryEntryKind\n}\n\n/// The stable portable operation category stored in a [`FileError`].\npub struct FileOperation {\n /// Stable code identifying the attempted portable operation.\n pub code: i32\n}\n\n/// A stable portable recovery category stored in a [`FileError`].\npub struct FileReason {\n /// Stable code identifying the portable recovery reason.\n pub code: i32\n}\n\n/// An allocation-free portable failure naming the attempted operation and recovery reason.\n///\n/// # Details\n///\n/// Match or compare [`operationCode`] and [`reasonCode`] for portable recovery. [`providerCode`]\n/// may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions\n/// must not depend on it.\npub struct FileError {\n /// The operation that failed.\n pub operation: FileOperation\n /// The portable reason callers can recover by.\n pub reason: FileReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Constructs the regular-file [`DirectoryEntryKind`].\npub fn file() -> DirectoryEntryKind { return DirectoryEntryKind { code: 0 } }\n\n/// Constructs the directory [`DirectoryEntryKind`].\npub fn directory() -> DirectoryEntryKind { return DirectoryEntryKind { code: 1 } }\n\n/// Returns the stable code for a consumed [`DirectoryEntryKind`]: `0` for file, `1` for directory.\npub fn entryKindCode(kind: DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Reads the stable directory-entry kind code through a borrow.\nfn borrowedKindCode(kind: &DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Constructs regular-file metadata with the complete length in bytes.\npub fn fileInfo(byteLength: usize) -> FileInfo {\n return FileInfo { byteLength: byteLength }\n}\n\n/// Constructs the fieldless portable directory metadata value.\npub fn directoryInfo() -> DirectoryInfo { return DirectoryInfo {} }\n\n/// Constructs a directory entry by taking ownership of its complete child `path` and `kind`.\npub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntry {\n return DirectoryEntry { path: move path, kind: move kind }\n}\n\n/// Selects the read-file operation.\npub fn readFileOperation() -> FileOperation { return FileOperation { code: 0 } }\n\n/// Selects the write-file operation.\npub fn writeFileOperation() -> FileOperation { return FileOperation { code: 1 } }\n\n/// Selects the stat operation.\npub fn statOperation() -> FileOperation { return FileOperation { code: 2 } }\n\n/// Selects the list-directory operation.\npub fn listDirectoryOperation() -> FileOperation { return FileOperation { code: 3 } }\n\n/// Selects the create-directory operation.\npub fn createDirectoryOperation() -> FileOperation { return FileOperation { code: 4 } }\n\n/// Selects the remove-file operation.\npub fn removeFileOperation() -> FileOperation { return FileOperation { code: 5 } }\n\n/// Selects the remove-directory operation.\npub fn removeDirectoryOperation() -> FileOperation { return FileOperation { code: 6 } }\n\n/// Selects path construction and resolution.\npub fn pathOperation() -> FileOperation { return FileOperation { code: 7 } }\n\n/// Selects the create-temporary-directory operation.\npub fn createTemporaryDirectoryOperation() -> FileOperation { return FileOperation { code: 8 } }\n\n/// Returns the stable numeric code of a consumed [`FileOperation`].\npub fn operationCode(operation: FileOperation) -> i32 { return operation.code }\n\n/// Constructs the `NotFound` recovery reason.\npub fn notFound() -> FileReason { return FileReason { code: 0 } }\n\n/// Constructs the `AlreadyExists` recovery reason.\npub fn alreadyExists() -> FileReason { return FileReason { code: 1 } }\n\n/// Constructs the `PermissionDenied` recovery reason.\npub fn permissionDenied() -> FileReason { return FileReason { code: 2 } }\n\n/// Constructs the `InvalidPath` recovery reason.\npub fn invalidPath() -> FileReason { return FileReason { code: 3 } }\n\n/// Constructs the `WrongType` recovery reason.\npub fn wrongType() -> FileReason { return FileReason { code: 4 } }\n\n/// Constructs the `NotEmpty` recovery reason.\npub fn notEmpty() -> FileReason { return FileReason { code: 5 } }\n\n/// Constructs the `NoSpace` recovery reason.\npub fn noSpace() -> FileReason { return FileReason { code: 6 } }\n\n/// Constructs the `TooLarge` recovery reason.\npub fn tooLarge() -> FileReason { return FileReason { code: 7 } }\n\n/// Constructs the `Unsupported` recovery reason.\npub fn unsupported() -> FileReason { return FileReason { code: 8 } }\n\n/// Constructs the catch-all `Other` recovery reason.\npub fn other() -> FileReason { return FileReason { code: 9 } }\n\n/// Returns the stable numeric code of a consumed [`FileReason`].\npub fn reasonCode(reason: FileReason) -> i32 { return reason.code }\n\n/// Constructs a portable [`FileError`] without a provider-specific numeric detail.\npub fn error(operation: FileOperation, reason: FileReason) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false,\n }\n}\n\n/// Constructs a portable [`FileError`] while retaining one provider-specific diagnostic code.\n///\n/// # Details\n///\n/// The numeric `code` is opaque outside that provider. The portable `operation` and `reason` remain\n/// the fields callers should use for recovery.\npub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true,\n }\n}\n\n/// Borrows an error and returns its provider-specific numeric detail, if one was retained.\npub fn providerCode(error: &FileError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\neffect fn raise(error: FileError) -> never ! FileError { fail move error }\n\neffect fn rejectPath() -> never ! FileError {\n fail error(pathOperation(), invalidPath())\n}\n\nfn byte(value: u8) -> i32 { return u8.toI32(value) }\n\nfn containsNul(values: &[u8]) -> bool {\n let mut index = usize.ZERO\n while index < values.length {\n if values[index] == u8.toU8(0) { return true }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validUtf8(values: &[u8]) -> bool {\n let decoded = stringFromUtf8(values)\n return match move decoded {\n Result.Success { value: text } => true\n Result.Failure { error: invalid } => false\n }\n}\n\nfn isDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != usize.ONE { return false }\n return byte(values[start]) == 46\n}\n\nfn isDotDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != 2 { return false }\n if byte(values[start]) != 46 { return false }\n return byte(values[start + usize.ONE]) == 46\n}\n\nfn validAbsolute(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) != 47 { return false }\n if containsNul(values) { return false }\n if values.length == usize.ONE { return true }\n let mut start = usize.ONE\n let mut index = usize.ONE\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validRelativeFragment(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) == 47 { return false }\n if containsNul(values) { return false }\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\neffect fn appendRange(\n target: Bytes,\n source: &[u8],\n start: usize,\n end: usize\n) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = move target\n let mut index = start\n while index < end {\n let one = [source[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\nfn finalNameStart(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ONE }\n let mut index = values.length\n while usize.ZERO < index {\n index = index - usize.ONE\n if byte(values[index]) == 47 { return index + usize.ONE }\n }\n return usize.ZERO\n}\n\neffect fn finishPath(bytes: Bytes) -> Path ! OutOfMemoryError ? &mut Allocator {\n let view = bytesAsSlice(&bytes)\n let start = finalNameStart(view)\n let nameBytes = run appendRange(bytesMake(), view, start, view.length)\n return Path { bytes: move bytes, nameBytes: move nameBytes }\n}\n\n/// Copies UTF-8 text into an owned, normalized provider-absolute [`Path`].\n///\n/// # Details\n///\n/// The text must begin with `/`. Root is valid; every other path must have nonempty components and\n/// no trailing slash, NUL, `.` component, or `..` component. Invalid input fails with\n/// `FileError(pathOperation(), invalidPath())`; copying can fail with [`OutOfMemoryError`].\npub effect fn make(value: string) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let values = stringUtf8Bytes(value)\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Constructs an owned normalized provider-absolute Path from exact platform bytes.\n///\n/// # Details\n///\n/// Platform paths are byte sequences, and a caller that received one from the platform — a\n/// directory entry, an argument, an environment value — must be able to hand it back unchanged.\n/// The same normalization applies as for textual construction: the value is absolute, rejects NUL,\n/// and rejects `.`, `..`, empty components, and trailing separators. Well-formed text is not\n/// required, so a Path built this way may have no `string` view.\npub effect fn fromBytes(values: &[u8]) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Allocates the portable root path `/` in the selected allocator.\npub effect fn root() -> Path ! OutOfMemoryError ? &mut Allocator {\n let mut copied = bytesMake()\n let appended = run bytesAppend(&mut copied, stringUtf8Bytes("/"))\n return run finishPath(move copied)\n}\n\nfn pathBytes(self: &Path) -> &[u8] { return bytesAsSlice(&self.bytes) }\n\n/// Borrows the complete normalized path as exact platform bytes.\n///\n/// # Details\n///\n/// This is the lossless view. It round-trips a Path built from platform bytes even when those\n/// bytes are not well-formed text, which the `string` view cannot promise.\npub fn rawBytes(self: &Path) -> &[u8] {\n return pathBytes(self)\n}\n\n/// Borrows the complete path as text when its bytes are known to be valid UTF-8.\n///\n/// # Details\n///\n/// Paths from [`make`], [`join`], [`joinUtf8`], and [`resolve`] satisfy that precondition. A path\n/// created with [`fromBytes`] may not; use [`rawBytes`] unless the source bytes were validated.\npub fn view(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(pathBytes(self)) }\n return ""\n}\n\n/// Returns `true` exactly when this path is the portable root `/`.\npub fn isRoot(self: &Path) -> bool { return bytesAsSlice(&self.bytes).length == usize.ONE }\n\n/// Borrows the final component as text; root returns empty text.\n///\n/// # Details\n///\n/// This has the same UTF-8 precondition as [`view`]. It does not allocate or include a separator.\npub fn name(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(bytesAsSlice(&self.nameBytes)) }\n return ""\n}\n\neffect fn joinBytes(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validRelativeFragment(fragment) == false { return run rejectPath() }\n let baseBytes = pathBytes(base)\n let mut combined = run appendRange(bytesMake(), baseBytes, usize.ZERO, baseBytes.length)\n if isRoot(base) == false {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeChild = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeChild, fragment, usize.ZERO, fragment.length)\n return run finishPath(move combined)\n}\n\n/// Appends one normalized relative text fragment to an absolute base path.\n///\n/// # Details\n///\n/// `fragment` must be nonempty and relative, with no NUL, empty, `.`, or `..` component and no\n/// trailing slash. Use [`resolve`] when dot components should be interpreted instead of rejected.\npub effect fn join(\n base: &Path,\n fragment: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return run joinBytes(base, stringUtf8Bytes(fragment))\n}\n\n/// Validates UTF-8 bytes as one normalized relative fragment and appends them to `base`.\n///\n/// # Details\n///\n/// This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the\n/// same malformed components rejected by [`join`] fail with the `InvalidPath` reason.\npub effect fn joinUtf8(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validUtf8(fragment) == false { return run rejectPath() }\n return run joinBytes(base, fragment)\n}\n\nfn componentCount(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ZERO }\n let mut count = usize.ONE\n let mut index = usize.ONE\n while index < values.length {\n if byte(values[index]) == 47 { count = count + usize.ONE }\n index = index + usize.ONE\n }\n return count\n}\n\nfn survivingRelative(values: &[u8], after: usize) -> bool {\n let mut depth = usize.ONE\n let mut start = after\n let mut index = after\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDotDot(values, start, index) {\n depth = depth - usize.ONE\n if depth == usize.ZERO { return false }\n } else {\n if isDot(values, start, index) == false { depth = depth + usize.ONE }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return true\n}\n\n/// Resolves relative text lexically against an explicit absolute base.\n///\n/// # Details\n///\n/// Empty text and `.` keep the base; `..` removes components; ordinary components append. An\n/// absolute relative value, an empty interior component, NUL, or any attempt to escape above root\n/// fails with the `InvalidPath` reason. Resolution is lexical and never accesses the filesystem.\npub effect fn resolve(\n base: &Path,\n relativeText: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let relative = stringUtf8Bytes(relativeText)\n if containsNul(relative) { return run rejectPath() }\n if usize.ZERO < relative.length {\n if byte(relative[usize.ZERO]) == 47 { return run rejectPath() }\n }\n let baseBytes = pathBytes(base)\n let mut keptBase = componentCount(baseBytes)\n let mut relativeDepth = usize.ZERO\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start == index {\n if index != relative.length { return run rejectPath() }\n } else {\n if isDotDot(relative, start, index) {\n if usize.ZERO < relativeDepth {\n relativeDepth = relativeDepth - usize.ONE\n } else {\n if keptBase == usize.ZERO { return run rejectPath() }\n keptBase = keptBase - usize.ONE\n }\n } else {\n if isDot(relative, start, index) == false {\n relativeDepth = relativeDepth + usize.ONE\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n let mut combined = bytesMake()\n let rooted = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n let mut included = usize.ZERO\n start = usize.ONE\n index = usize.ONE\n while index <= baseBytes.length {\n let mut boundary = false\n if index == baseBytes.length {\n boundary = true\n } else {\n if byte(baseBytes[index]) == 47 { boundary = true }\n }\n if boundary {\n if included < keptBase {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeBaseComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeBaseComponent, baseBytes, start, index)\n included = included + usize.ONE\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n start = usize.ZERO\n index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDot(relative, start, index) == false {\n if isDotDot(relative, start, index) == false {\n if survivingRelative(relative, index + usize.ONE) {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeRelativeComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeRelativeComponent, relative, start, index)\n included = included + usize.ONE\n }\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return run finishPath(move combined)\n}\n\n/// Allocates an independently owned parent path, or [`None`] when `self` is root.\n///\n/// # Details\n///\n/// The result does not borrow `self`. A direct child of root has root as its parent.\npub effect fn parent(\n self: &Path\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n if isRoot(self) { return none() }\n let values = pathBytes(self)\n let nameStart = finalNameStart(values)\n let mut end = usize.ONE\n if nameStart != usize.ONE { end = nameStart - usize.ONE }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, end)\n let owned = run finishPath(move copied)\n return some(move owned)\n}\n\n/// Portable mutable service for normalized paths and whole-file operations.\n///\n/// # Details\n///\n/// Application code supplies one provider lexically with `Effect.provideMut`; tests can implement\n/// this service in memory, while native applications can use `silk.os_filesystem`. The service owns\n/// platform policy, but every implementation must preserve the portable error categories,\n/// create-or-truncate writes, and deterministic listing order described here.\n///\n/// # Examples\n/// ## Write a file after creating its parents\n/// ```silk\n/// import silk.allocator { Allocator }\n///\n/// import silk.filesystem as FileSystem\n///\n/// import silk.usize as usize\n///\n/// pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize\n/// ! FileSystem.FileError | Allocator.OutOfMemoryError\n/// ? &mut FileSystem.FileSystem | &mut Allocator {\n/// let written = run FileSystem.writeFileWithParents(path, contents)\n/// return contents.length\n/// }\n/// ```\npub service FileSystem {\n /// Reads one complete regular file into independently owned bytes.\n ///\n /// # Details\n ///\n /// Reading a directory fails with `WrongType`. Allocation of the returned [`Bytes`] may fail\n /// independently of the provider read.\n effect fn readFile(\n path: &Path\n ) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Writes one complete byte view with create-or-truncate semantics.\n ///\n /// # Details\n ///\n /// A missing file is created; an existing regular file is replaced by exactly `bytes`. The call\n /// does not create missing parent directories—use [`writeFileWithParents`] for that workflow.\n effect fn writeFile(path: &Path, bytes: &[u8]) -> () ! FileError ? &mut FileSystem\n /// Returns [`FileInfo`] or [`DirectoryInfo`] for the path without opening file contents.\n ///\n /// # Details\n ///\n /// Missing paths fail with `NotFound`; providers use `WrongType` only when an operation requires a\n /// particular kind, not for this discriminating query.\n effect fn stat(path: &Path) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem\n /// Returns immediate owned children in deterministic complete-path byte order.\n ///\n /// # Details\n ///\n /// The result is not recursive. Each `DirectoryEntry.path` is independently owned and may be\n /// retained after the listing vector is released.\n effect fn listDirectory(\n path: &Path\n ) -> Vector ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Creates exactly one missing directory whose parent already exists.\n ///\n /// # Details\n ///\n /// Existing paths fail with `AlreadyExists`; use [`createDirectoriesRecursively`] to ensure every\n /// missing component.\n effect fn createDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one regular file and fails with `WrongType` for a directory.\n effect fn removeFile(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one empty directory.\n ///\n /// # Details\n ///\n /// A nonempty directory fails with `NotEmpty`; use [`removeDirectoryRecursively`] only when all\n /// descendants are intentionally in scope for removal.\n effect fn removeDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Creates one directory under an existing parent under a name no other caller holds.\n ///\n /// # Details\n ///\n /// The provider chooses the name\'s unique part and returns the complete Path, because only the\n /// provider can create and claim a name in one step. A caller that supplied the name would have\n /// to check-then-create, and the gap between those two is exactly the race this avoids.\n /// `prefix` is a byte prefix for the provider-chosen child name, not a complete path. The returned\n /// directory already exists and is an immediate child of `parent`.\n effect fn createTemporaryDirectory(\n parent: &Path,\n prefix: &[u8]\n ) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n}\n\n/// A directory a caller owns outright, together with everything written inside it.\n///\n/// # Details\n///\n/// Ownership is affine: `TemporaryDirectory` holds an owned `Path`, so exactly one binding holds\n/// it and the compiler rejects a second use of a moved one. Ownership is not, however, a `Drop`\n/// hook. Removing a directory is a fallible operation that requires the `FileSystem` capability,\n/// and a `Drop` hook may carry neither a failure row nor a requirement row, so a hook here could\n/// only be written by inventing an infallible intrinsic over a fallible syscall. Release is\n/// therefore explicit and honest about both rows — see `release`.\n///\n/// Scope ownership comes from composition rather than from a hook: `Effect.ensuring(release)`\n/// runs the release whatever the protected Effect\'s outcome. Because `ensuring` types its\n/// finalizer `! never`, that composition has to say what a failed removal means; `releaseIgnored`\n/// is the stdlib\'s answer and names the loss at the call site.\npub struct TemporaryDirectory {\n /// The complete owned path callers use while the scope remains live.\n pub path: Path\n}\n\n/// Creates an explicitly owned temporary directory under `parent` with a name beginning in `prefix`.\n///\n/// # Details\n///\n/// The result is owned. Nothing removes it until a caller runs [`release`] or [`releaseIgnored`].\n/// The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in\n/// one operation.\npub effect fn temporaryDirectory(\n parent: &Path,\n prefix: string\n) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let created = run FileSystem.createTemporaryDirectory(parent, stringUtf8Bytes(prefix))\n return TemporaryDirectory { path: move created }\n}\n\n/// Consumes one TemporaryDirectory and removes it together with everything inside it.\n///\n/// # Details\n///\n/// Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the\n/// tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a\n/// failed cleanup uses this operation and handles the failure. The owner is consumed even when\n/// removal fails, so copy any diagnostic path information needed before calling.\npub effect fn release(\n self: TemporaryDirectory\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let owned = move self\n let removed = run removeDirectoryRecursively(&owned.path)\n drop owned\n return ()\n}\n\neffect fn discardReleaseFailure(error: FileError | OutOfMemoryError) -> () { return () }\n\n/// Consumes one TemporaryDirectory, removes it, and discards a failed removal.\n///\n/// # Details\n///\n/// This exists because `Effect.ensuring` types its finalizer `! never`, so a fallible release has\n/// to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a\n/// caller reading `releaseIgnored` at the call site can see that a failed removal is being\n/// dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —\n/// a directory the host will reap — and what is kept is the protected Effect\'s own outcome, which\n/// is the answer the program was computing.\n///\n/// A caller who needs the failure uses `release` instead and does not compose it with `ensuring`.\n///\n/// The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer\n/// starts. Derive the required paths before you give the owner to the finalizer.\npub effect fn releaseIgnored(\n self: TemporaryDirectory\n) -> () ? &mut FileSystem | &mut Allocator {\n return run Effect.catchAll(release(move self), discardReleaseFailure)\n}\n\n/// Copies one recorded Path out of the walk\'s own record.\n///\n/// The walk appends to the same record it is reading, so it reads through a copy rather than\n/// through a borrow that the next append would invalidate.\neffect fn recordedCopy(\n recorded: &Vector,\n index: usize\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return match &vectorAsSlice(recorded)[index] {\n Path { bytes, nameBytes } => run fromBytes(bytesAsSlice(&bytes))\n }\n}\n\n/// Removes a directory, every descendant file, and every descendant directory.\n///\n/// # Details\n///\n/// Two passes, because the portable primitive removes exactly one *empty* directory. The first\n/// pass walks the tree front to back, unlinking every file it meets and recording every directory\n/// it meets; the second removes the recorded directories back to front. That order is\n/// child-before-parent for free: a directory is always recorded before the children found inside\n/// it, so reversing the record reverses the containment. Neither pass recurses, so depth costs\n/// vector capacity rather than stack.\n///\n/// This operation is destructive and not transactional. If a provider or allocation failure occurs,\n/// removals already completed remain completed and the remaining tree is left in place.\npub effect fn removeDirectoryRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let mut recorded = vectorMake()\n let seed = run fromBytes(rawBytes(path))\n let noted = run vectorAppend(&mut recorded, move seed)\n let mut index = usize.ZERO\n while index < vectorLength(&recorded) {\n let current = run recordedCopy(&recorded, index)\n let entries = run FileSystem.listDirectory(¤t)\n let listed = vectorAsSlice(&entries)\n let mut cursor = usize.ZERO\n while cursor < listed.length {\n let childKind = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } => borrowedKindCode(&childEntryKind)\n }\n if childKind == 0 {\n let unlinked = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run FileSystem.removeFile(&childPath)\n }\n } else {\n let toRemove = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run fromBytes(rawBytes(&childPath))\n }\n let notedChild = run vectorAppend(&mut recorded, move toRemove)\n }\n cursor = cursor + usize.ONE\n }\n index = index + usize.ONE\n }\n while usize.ZERO < vectorLength(&recorded) {\n let taken = vectorPop(&mut recorded)\n let emptied = match move taken {\n Option.Some { value: selected } => move selected\n Option.None => run fromBytes(rawBytes(path))\n }\n let removed = run FileSystem.removeDirectory(&emptied)\n }\n return ()\n}\n\nstruct DirectoryPresent {}\nstruct DirectoryMissing {}\nstruct DirectoryWrongType {}\nstruct DirectoryStatFailure { error: FileError }\n\nfn classifyStatFailure(\n failure: FileError\n) -> DirectoryMissing | DirectoryStatFailure {\n if failure.reason.code == 0 { return DirectoryMissing {} }\n return DirectoryStatFailure { error: move failure }\n}\n\nfn classifyDirectory(\n outcome: Result\n) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure {\n return match move outcome {\n Result.Success { value: info } => match move info {\n DirectoryInfo {} => DirectoryPresent {}\n FileInfo { byteLength } => DirectoryWrongType {}\n }\n Result.Failure { error: failure } => classifyStatFailure(move failure)\n }\n}\n\n/// Ensures that `path` and every missing ancestor exist as directories.\n///\n/// # Details\n///\n/// Existing directories are kept. An existing regular file at any component fails with\n/// `WrongType`; failures other than `NotFound` propagate. This is ordinary stat-then-create\n/// composition, so concurrent namespace changes may still race according to provider policy.\npub effect fn createDirectoriesRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let values = pathBytes(path)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Effect.result(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return ()\n}\n\n/// Ensures every parent directory exists, then writes the complete byte view to `path`.\n///\n/// # Details\n///\n/// The final write uses `FileSystem.writeFile` create-or-truncate semantics. Passing root delegates\n/// directly to the provider and normally fails with `WrongType`. Directory creation and writing are\n/// not transactional, so a later failure may leave newly created parents behind.\npub effect fn writeFileWithParents(\n path: &Path,\n bytes: &[u8]\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n if isRoot(path) { return run FileSystem.writeFile(path, bytes) }\n let pathValues = pathBytes(path)\n let nameStart = finalNameStart(pathValues)\n let mut parentEnd = usize.ONE\n if nameStart != usize.ONE { parentEnd = nameStart - usize.ONE }\n let parentBytes = run appendRange(bytesMake(), pathValues, usize.ZERO, parentEnd)\n let ownedParent = run finishPath(move parentBytes)\n let values = pathBytes(&ownedParent)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Effect.result(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return run FileSystem.writeFile(path, bytes)\n}\n\neffect fn existsFailure(failure: FileError) -> bool ! FileError {\n if failure.reason.code == 0 { return false }\n return run raise(move failure)\n}\n\n/// Returns whether a file or directory exists at `path`.\n///\n/// # Details\n///\n/// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider\n/// failure propagate so callers cannot mistake an inaccessible path for an absent one.\npub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem {\n let completed = run Effect.result(FileSystem.stat(path))\n return match move completed {\n Result.Success { value: info } => true\n Result.Failure { error: failure } => run existsFailure(move failure)\n }\n}\n', }, { module: 'silk/format', @@ -733,14 +733,14 @@ export const modules = [ module: 'silk/logger', path: 'silk/logger.silk', sourceIdentity: 'silk/logger', - digest: 'c1461739a9cc1036b957df65677162a37ab17f442196292cd6912866c4923605', + digest: 'bbdf957403f1e0fb07ea520391fc524b045ee388f317ba0b81f548f73f49732e', documentation: 'silk/logger.silk', layer: 'portable', - runtimeInventory: ['bindRequirementMut', 'effectResult', 'replace'], + runtimeInventory: ['bindRequirementMut', 'replace'], namespace: 'Logger', aliases: ['InMemoryLogger', 'LogError', 'LogLevel', 'StdoutLogger'], source: - '//! Typed semantic logging with replaceable stdout and bounded in-memory providers.\n//!\n//! # When to use\n//! Require [`Logger`] when code emits whole semantic messages but should not choose storage or\n//! destination. Provide [`StdoutLogger`] at a process edge. Use [`InMemoryLogger`] for\n//! deterministic observation and failure tests.\n//!\n//! # Details\n//! Each invocation carries one [`LogLevel`] and one valid UTF-8 message. The service does not add\n//! formatting, newlines, timestamps, or allocation requirements. The stdout provider forwards only\n//! the message bytes; the in-memory provider retains at most eight committed events and 64 message\n//! bytes, and exposes attempted calls separately from successful commits.\n//!\n//! # Gotchas\n//! Logger failures are typed [`LogError`] values and do not guarantee that a message committed.\n//! In-memory accessors require an event index less than [`length`] and a valid message-byte index.\n//!\n//! # Examples\n//! ## Record and inspect one warning\n//!\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! import silk.logger { Logger }\n//! import silk.logger { LogLevel }\n//!\n//! import silk.usize as usize\n//!\n//! effect fn program() -> i32\n//! ! Logger.LogError {\n//! let mut logger = Logger.inMemoryProvider()\n//! let logged = run Effect.logWarning("cache miss")\n//! |> Effect.provideMut(&mut logger)\n//! if Logger.length(&logger) != usize.ONE {\n//! return 1\n//! }\n//! if Logger.levelAt(&logger, usize.ZERO) != LogLevel.Warning {\n//! return 2\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Logger.LogError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.i32 as i32\nimport silk.result { Result }\nimport silk.standard_streams {\n NativeStandardStreams,\n StreamWriteError,\n nativeStandardStreamProvider as nativeStreams,\n send\n}\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// One closed logging severity from Trace through Error.\npub enum LogLevel {\n /// Detailed diagnostic events.\n Trace,\n /// Development diagnostic events.\n Debug,\n /// Ordinary operational events.\n Info,\n /// Recoverable abnormal conditions.\n Warning,\n /// Operations that did not complete as intended.\n Error\n}\n\n/// A typed failure reported by one [`Logger`] provider.\n///\n/// # Details\n///\n/// The numeric code belongs to the provider. Portable code can recover from `LogError` without\n/// assigning one meaning to that code across different providers.\npub struct LogError {\n code: i32\n}\n\n/// Returns the provider-defined failure code for diagnostics.\n///\n/// # Gotchas\n///\n/// Interpret this code only with knowledge of the selected provider. Different providers can use\n/// the same code for different failures.\npub fn errorCode(error: LogError) -> i32 { return error.code }\n\neffect fn reject(code: i32) -> never ! LogError {\n fail LogError { code: code }\n}\n\n/// A replaceable service that receives one complete semantic log event per call.\n///\n/// # When to use\n///\n/// Use this service when library code must emit events without selecting stdout, memory, or another\n/// destination.\n///\n/// # Details\n///\n/// Each call carries one severity and one valid UTF-8 message. The service does not require a\n/// newline, timestamp, prefix, allocation, or output destination. The provider owns those choices.\npub service Logger {\n /// Submits one complete UTF-8 message at one severity to the active provider.\n ///\n /// # Details\n ///\n /// The call preserves the message bytes exactly. It does not add a newline, severity label,\n /// timestamp, or other formatting. A provider failure produces [`LogError`].\n effect fn log(\n level: LogLevel,\n message: string\n ) -> () ! LogError ? &mut Logger\n}\n\n/// A [`Logger`] provider that writes each complete message to process standard output.\n///\n/// # Details\n///\n/// The provider ignores the severity for physical formatting and writes only the UTF-8 message\n/// bytes. It adds no newline and performs no message allocation.\npub struct StdoutLogger {}\n\n/// Creates a logger that forwards each complete message to process standard output.\n///\n/// # Gotchas\n///\n/// The caller must include a newline in `message` when line separation is required. A standard-\n/// output write failure becomes a provider-defined [`LogError`].\npub fn stdoutProvider() -> StdoutLogger { return StdoutLogger {} }\n\neffect fn writeStdoutCounted(\n streams: &mut NativeStandardStreams,\n message: &[u8]\n) -> i32 ! StreamWriteError {\n let written = run Intrinsic.bindRequirementMut(send(false, message), streams)\n return 0\n}\n\neffect fn writeStdout(\n self: &mut StdoutLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let mut streams = nativeStreams()\n let bytes = stringUtf8Bytes(message)\n let completed = run Intrinsic.effectResult(writeStdoutCounted(&mut streams, bytes))\n return match move completed {\n Result.Success { value: success } => ()\n Result.Failure { error: failure } => run reject(3)\n }\n}\n\nimpl Logger for StdoutLogger {\n log: StdoutLogger.writeStdout\n}\n\n/// A deterministic [`Logger`] provider that retains up to eight events and 64 total message bytes.\n///\n/// # When to use\n///\n/// Use this provider in tests that must inspect event order, severity, message bytes, or failure\n/// behavior without process output.\n///\n/// # Details\n///\n/// The provider copies each committed message into fixed internal storage. It records attempted\n/// calls separately from committed events. Capacity failure and configured failure do not commit an\n/// event.\npub struct InMemoryLogger {\n levels: [LogLevel; 8]\n offsets: [usize; 8]\n lengths: [usize; 8]\n messages: [i32; 64]\n count: usize\n messageLength: usize\n attempts: usize\n failEnabled: bool\n failAt: usize\n}\n\nfn emptyLevels() -> [LogLevel; 8] {\n return [\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace\n ]\n}\n\nfn emptyIndexes() -> [usize; 8] { return [0, 0, 0, 0, 0, 0, 0, 0] }\n\nfn emptyMessages() -> [i32; 64] {\n return [\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n ]\n}\n\n/// Creates an empty in-memory logger with capacity for eight events and 64 message bytes.\n///\n/// # Gotchas\n///\n/// A call fails when eight events are already committed. A call also fails when its bytes exceed\n/// the remaining 64-byte total. Neither failure commits the event.\npub fn inMemoryProvider() -> InMemoryLogger {\n return InMemoryLogger {\n levels: emptyLevels(),\n offsets: emptyIndexes(),\n lengths: emptyIndexes(),\n messages: emptyMessages(),\n count: usize.add(0, 0),\n messageLength: usize.add(0, 0),\n attempts: usize.add(0, 0),\n failEnabled: false,\n failAt: usize.add(0, 0),\n }\n}\n\n/// Creates an in-memory logger that rejects one zero-based attempted-call ordinal.\n///\n/// # Details\n///\n/// The configured attempt increases [`attempts`] but does not increase [`length`] or consume\n/// message capacity. Other attempts retain the eight-event and 64-byte limits of\n/// [`inMemoryProvider`].\npub fn inMemoryProviderFailAt(failAt: usize) -> InMemoryLogger {\n let mut logger = inMemoryProvider()\n logger.failEnabled = true\n logger.failAt = failAt\n return move logger\n}\n\neffect fn record(\n self: &mut InMemoryLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let values = stringUtf8Bytes(message)\n let ordinal = self.attempts\n self.attempts = self.attempts + usize.add(0, 1)\n if self.failEnabled {\n if ordinal == self.failAt { return run reject(1) }\n }\n\n if self.count == usize.add(0, 8) { return run reject(2) }\n if values.length > usize.add(0, 64) - self.messageLength { return run reject(2) }\n\n let offset = self.messageLength\n let mut messages = Intrinsic.replace(self.messages, emptyMessages())\n let mut index = usize.add(0, 0)\n while index < values.length {\n messages[offset + index] = u8.toI32(values[index])\n index = index + usize.add(0, 1)\n }\n let mut levels = Intrinsic.replace(self.levels, emptyLevels())\n let mut offsets = Intrinsic.replace(self.offsets, emptyIndexes())\n let mut lengths = Intrinsic.replace(self.lengths, emptyIndexes())\n levels[self.count] = level\n offsets[self.count] = offset\n lengths[self.count] = values.length\n self.messages = move messages\n self.levels = move levels\n self.offsets = move offsets\n self.lengths = move lengths\n self.count = self.count + usize.add(0, 1)\n self.messageLength = self.messageLength + values.length\n return ()\n}\n\nimpl Logger for InMemoryLogger {\n log: InMemoryLogger.record\n}\n\n/// Returns the number of events that the in-memory logger committed.\n///\n/// # Details\n///\n/// Failed attempts do not increase this count. Use [`attempts`] when rejected calls must also be\n/// observed.\npub fn length(self: &InMemoryLogger) -> usize {\n return self.count\n}\n\n/// Returns the severity of one committed event.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns the initial Trace\n/// value instead of trapping. An index of eight or more traps.\npub fn levelAt(self: &InMemoryLogger, index: usize) -> LogLevel {\n let levels = self.levels\n return levels[index]\n}\n\n/// Returns the UTF-8 byte length of one committed message.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns zero instead of\n/// trapping. An index of eight or more traps.\npub fn messageLengthAt(self: &InMemoryLogger, index: usize) -> usize {\n let lengths = self.lengths\n return lengths[index]\n}\n\n/// Returns one UTF-8 byte from a committed message.\n///\n/// # Gotchas\n///\n/// `eventIndex` must be less than [`length`]. `byteIndex` must be less than\n/// [`messageLengthAt`] for that event. An unused event or invalid byte index traps. An event index\n/// of eight or more also traps.\npub fn messageByteAt(\n self: &InMemoryLogger,\n eventIndex: usize,\n byteIndex: usize\n) -> u8 {\n let lengths = self.lengths\n let length = lengths[eventIndex]\n if length <= byteIndex { let boom = 1 / 0 }\n let offsets = self.offsets\n let offset = offsets[eventIndex]\n let messages = self.messages\n return i32.toU8(messages[offset + byteIndex])\n}\n\n/// Returns the number of calls attempted, including calls that produced [`LogError`].\npub fn attempts(self: &InMemoryLogger) -> usize { return self.attempts }\n', + '//! Typed semantic logging with replaceable stdout and bounded in-memory providers.\n//!\n//! # When to use\n//! Require [`Logger`] when code emits whole semantic messages but should not choose storage or\n//! destination. Provide [`StdoutLogger`] at a process edge. Use [`InMemoryLogger`] for\n//! deterministic observation and failure tests.\n//!\n//! # Details\n//! Each invocation carries one [`LogLevel`] and one valid UTF-8 message. The service does not add\n//! formatting, newlines, timestamps, or allocation requirements. The stdout provider forwards only\n//! the message bytes; the in-memory provider retains at most eight committed events and 64 message\n//! bytes, and exposes attempted calls separately from successful commits.\n//!\n//! # Gotchas\n//! Logger failures are typed [`LogError`] values and do not guarantee that a message committed.\n//! In-memory accessors require an event index less than [`length`] and a valid message-byte index.\n//!\n//! # Examples\n//! ## Record and inspect one warning\n//!\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! import silk.logger { Logger }\n//! import silk.logger { LogLevel }\n//!\n//! import silk.usize as usize\n//!\n//! effect fn program() -> i32\n//! ! Logger.LogError {\n//! let mut logger = Logger.inMemoryProvider()\n//! let logged = run Effect.logWarning("cache miss")\n//! |> Effect.provideMut(&mut logger)\n//! if Logger.length(&logger) != usize.ONE {\n//! return 1\n//! }\n//! if Logger.levelAt(&logger, usize.ZERO) != LogLevel.Warning {\n//! return 2\n//! }\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Logger.LogError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.result { Result }\nimport silk.standard_streams {\n NativeStandardStreams,\n StreamWriteError,\n nativeStandardStreamProvider as nativeStreams,\n send\n}\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// One closed logging severity from Trace through Error.\npub enum LogLevel {\n /// Detailed diagnostic events.\n Trace,\n /// Development diagnostic events.\n Debug,\n /// Ordinary operational events.\n Info,\n /// Recoverable abnormal conditions.\n Warning,\n /// Operations that did not complete as intended.\n Error\n}\n\n/// A typed failure reported by one [`Logger`] provider.\n///\n/// # Details\n///\n/// The numeric code belongs to the provider. Portable code can recover from `LogError` without\n/// assigning one meaning to that code across different providers.\npub struct LogError {\n code: i32\n}\n\n/// Returns the provider-defined failure code for diagnostics.\n///\n/// # Gotchas\n///\n/// Interpret this code only with knowledge of the selected provider. Different providers can use\n/// the same code for different failures.\npub fn errorCode(error: LogError) -> i32 { return error.code }\n\neffect fn reject(code: i32) -> never ! LogError {\n fail LogError { code: code }\n}\n\n/// A replaceable service that receives one complete semantic log event per call.\n///\n/// # When to use\n///\n/// Use this service when library code must emit events without selecting stdout, memory, or another\n/// destination.\n///\n/// # Details\n///\n/// Each call carries one severity and one valid UTF-8 message. The service does not require a\n/// newline, timestamp, prefix, allocation, or output destination. The provider owns those choices.\npub service Logger {\n /// Submits one complete UTF-8 message at one severity to the active provider.\n ///\n /// # Details\n ///\n /// The call preserves the message bytes exactly. It does not add a newline, severity label,\n /// timestamp, or other formatting. A provider failure produces [`LogError`].\n effect fn log(\n level: LogLevel,\n message: string\n ) -> () ! LogError ? &mut Logger\n}\n\n/// A [`Logger`] provider that writes each complete message to process standard output.\n///\n/// # Details\n///\n/// The provider ignores the severity for physical formatting and writes only the UTF-8 message\n/// bytes. It adds no newline and performs no message allocation.\npub struct StdoutLogger {}\n\n/// Creates a logger that forwards each complete message to process standard output.\n///\n/// # Gotchas\n///\n/// The caller must include a newline in `message` when line separation is required. A standard-\n/// output write failure becomes a provider-defined [`LogError`].\npub fn stdoutProvider() -> StdoutLogger { return StdoutLogger {} }\n\neffect fn writeStdoutCounted(\n streams: &mut NativeStandardStreams,\n message: &[u8]\n) -> i32 ! StreamWriteError {\n let written = run Intrinsic.bindRequirementMut(send(false, message), streams)\n return 0\n}\n\neffect fn writeStdout(\n self: &mut StdoutLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let mut streams = nativeStreams()\n let bytes = stringUtf8Bytes(message)\n let completed = run Effect.result(writeStdoutCounted(&mut streams, bytes))\n return match move completed {\n Result.Success { value: success } => ()\n Result.Failure { error: failure } => run reject(3)\n }\n}\n\nimpl Logger for StdoutLogger {\n log: StdoutLogger.writeStdout\n}\n\n/// A deterministic [`Logger`] provider that retains up to eight events and 64 total message bytes.\n///\n/// # When to use\n///\n/// Use this provider in tests that must inspect event order, severity, message bytes, or failure\n/// behavior without process output.\n///\n/// # Details\n///\n/// The provider copies each committed message into fixed internal storage. It records attempted\n/// calls separately from committed events. Capacity failure and configured failure do not commit an\n/// event.\npub struct InMemoryLogger {\n levels: [LogLevel; 8]\n offsets: [usize; 8]\n lengths: [usize; 8]\n messages: [i32; 64]\n count: usize\n messageLength: usize\n attempts: usize\n failEnabled: bool\n failAt: usize\n}\n\nfn emptyLevels() -> [LogLevel; 8] {\n return [\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace,\n LogLevel.Trace\n ]\n}\n\nfn emptyIndexes() -> [usize; 8] { return [0, 0, 0, 0, 0, 0, 0, 0] }\n\nfn emptyMessages() -> [i32; 64] {\n return [\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0,\n ]\n}\n\n/// Creates an empty in-memory logger with capacity for eight events and 64 message bytes.\n///\n/// # Gotchas\n///\n/// A call fails when eight events are already committed. A call also fails when its bytes exceed\n/// the remaining 64-byte total. Neither failure commits the event.\npub fn inMemoryProvider() -> InMemoryLogger {\n return InMemoryLogger {\n levels: emptyLevels(),\n offsets: emptyIndexes(),\n lengths: emptyIndexes(),\n messages: emptyMessages(),\n count: usize.add(0, 0),\n messageLength: usize.add(0, 0),\n attempts: usize.add(0, 0),\n failEnabled: false,\n failAt: usize.add(0, 0),\n }\n}\n\n/// Creates an in-memory logger that rejects one zero-based attempted-call ordinal.\n///\n/// # Details\n///\n/// The configured attempt increases [`attempts`] but does not increase [`length`] or consume\n/// message capacity. Other attempts retain the eight-event and 64-byte limits of\n/// [`inMemoryProvider`].\npub fn inMemoryProviderFailAt(failAt: usize) -> InMemoryLogger {\n let mut logger = inMemoryProvider()\n logger.failEnabled = true\n logger.failAt = failAt\n return move logger\n}\n\neffect fn record(\n self: &mut InMemoryLogger,\n level: LogLevel,\n message: string\n) -> () ! LogError {\n let values = stringUtf8Bytes(message)\n let ordinal = self.attempts\n self.attempts = self.attempts + usize.add(0, 1)\n if self.failEnabled {\n if ordinal == self.failAt { return run reject(1) }\n }\n\n if self.count == usize.add(0, 8) { return run reject(2) }\n if values.length > usize.add(0, 64) - self.messageLength { return run reject(2) }\n\n let offset = self.messageLength\n let mut messages = Intrinsic.replace(self.messages, emptyMessages())\n let mut index = usize.add(0, 0)\n while index < values.length {\n messages[offset + index] = u8.toI32(values[index])\n index = index + usize.add(0, 1)\n }\n let mut levels = Intrinsic.replace(self.levels, emptyLevels())\n let mut offsets = Intrinsic.replace(self.offsets, emptyIndexes())\n let mut lengths = Intrinsic.replace(self.lengths, emptyIndexes())\n levels[self.count] = level\n offsets[self.count] = offset\n lengths[self.count] = values.length\n self.messages = move messages\n self.levels = move levels\n self.offsets = move offsets\n self.lengths = move lengths\n self.count = self.count + usize.add(0, 1)\n self.messageLength = self.messageLength + values.length\n return ()\n}\n\nimpl Logger for InMemoryLogger {\n log: InMemoryLogger.record\n}\n\n/// Returns the number of events that the in-memory logger committed.\n///\n/// # Details\n///\n/// Failed attempts do not increase this count. Use [`attempts`] when rejected calls must also be\n/// observed.\npub fn length(self: &InMemoryLogger) -> usize {\n return self.count\n}\n\n/// Returns the severity of one committed event.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns the initial Trace\n/// value instead of trapping. An index of eight or more traps.\npub fn levelAt(self: &InMemoryLogger, index: usize) -> LogLevel {\n let levels = self.levels\n return levels[index]\n}\n\n/// Returns the UTF-8 byte length of one committed message.\n///\n/// # Gotchas\n///\n/// `index` must be less than [`length`]. An unused index below eight returns zero instead of\n/// trapping. An index of eight or more traps.\npub fn messageLengthAt(self: &InMemoryLogger, index: usize) -> usize {\n let lengths = self.lengths\n return lengths[index]\n}\n\n/// Returns one UTF-8 byte from a committed message.\n///\n/// # Gotchas\n///\n/// `eventIndex` must be less than [`length`]. `byteIndex` must be less than\n/// [`messageLengthAt`] for that event. An unused event or invalid byte index traps. An event index\n/// of eight or more also traps.\npub fn messageByteAt(\n self: &InMemoryLogger,\n eventIndex: usize,\n byteIndex: usize\n) -> u8 {\n let lengths = self.lengths\n let length = lengths[eventIndex]\n if length <= byteIndex { let boom = 1 / 0 }\n let offsets = self.offsets\n let offset = offsets[eventIndex]\n let messages = self.messages\n return i32.toU8(messages[offset + byteIndex])\n}\n\n/// Returns the number of calls attempted, including calls that produced [`LogError`].\npub fn attempts(self: &InMemoryLogger) -> usize { return self.attempts }\n', }, { module: 'silk/metrics', @@ -843,12 +843,11 @@ export const modules = [ module: 'silk/os_filesystem', path: 'silk/os_filesystem.silk', sourceIdentity: 'silk/os_filesystem', - digest: '6a7645105b42d3cd27e90902d858119ed9f6fdd538e0fce9d071e9ab31055121', + digest: 'fff840f77d16835ec8ee4fc925fc3d95e1d71fd748a441e548faf9c259c1d285', documentation: 'silk/os_filesystem.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], runtimeInventory: [ - 'effectResult', 'osDirectoryCreate', 'osDirectoryCreateUnique', 'osDirectoryNext', @@ -863,7 +862,7 @@ export const modules = [ ], namespace: 'OsFileSystem', source: - '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut received = none()\n unsafe {\n received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode)\n }\n let length = match move received {\n Option.None => run raise(readFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Intrinsic.effectResult(readLoop(&mut handle))\n let closed = run Intrinsic.effectResult(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut written = none()\n unsafe {\n written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode)\n }\n let length = match move written {\n Option.None => run raise(writeFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Intrinsic.effectResult(writeLoop(&mut handle, bytes))\n let closed = run Intrinsic.effectResult(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut next = none()\n unsafe {\n next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode)\n }\n let encodedLength = match move next {\n Option.Some { value: presentLength } => presentLength + usize.ONE\n Option.None => usize.ZERO\n }\n if encodedLength == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let length = encodedLength - usize.ONE\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Intrinsic.effectResult(listLoop(&mut handle, path))\n let closed = run Intrinsic.effectResult(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, output, required, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n let length = match move chosen {\n Option.Some { value: selected } => selected\n Option.None => usize.ZERO\n }\n if length == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', + '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.effect { Effect }\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut received = none()\n unsafe {\n received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode)\n }\n let length = match move received {\n Option.None => run raise(readFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Effect.result(readLoop(&mut handle))\n let closed = run Effect.result(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut written = none()\n unsafe {\n written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode)\n }\n let length = match move written {\n Option.None => run raise(writeFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Effect.result(writeLoop(&mut handle, bytes))\n let closed = run Effect.result(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut next = none()\n unsafe {\n next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode)\n }\n let encodedLength = match move next {\n Option.Some { value: presentLength } => presentLength + usize.ONE\n Option.None => usize.ZERO\n }\n if encodedLength == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let length = encodedLength - usize.ONE\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Effect.result(listLoop(&mut handle, path))\n let closed = run Effect.result(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, output, required, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n let length = match move chosen {\n Option.Some { value: selected } => selected\n Option.None => usize.ZERO\n }\n if length == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', }, { module: 'silk/os_host_input', diff --git a/packages/compiler/src/Suspension.ts b/packages/compiler/src/Suspension.ts index 895a9bab6..0606f6552 100644 --- a/packages/compiler/src/Suspension.ts +++ b/packages/compiler/src/Suspension.ts @@ -70,17 +70,9 @@ export type SuspensionCompletion = | { readonly _tag: 'Reify' readonly outcome: Type.Effect - readonly resultType: Type.Nominal - readonly resultField: DeclarationFacts.FieldId - readonly resultUnion: Type.StructuralUnion - readonly successType: Type.Nominal - readonly successField: DeclarationFacts.FieldId - readonly successTag: number - readonly failureType: Type.Nominal - readonly failureField: DeclarationFacts.FieldId - readonly failureTag: number + readonly successType: Type.Type readonly failureValueType: Type.Type - readonly resultShape: Layout.CallingShape + readonly successShape: Layout.CallingShape readonly outcomeShape: Layout.CallingShape readonly failureValueShape: Layout.CallingShape } diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 368107f21..3407eeb51 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -4704,6 +4704,22 @@ const emitShortCircuitOperation = ( ] } +const emitConditionalOperation = ( + operation: Extract, + state: WasmOperationContext, +): ReadonlyArray => { + const branch = (selected: typeof operation.taken): ReadonlyArray => [ + ...selected.operations.flatMap((nested) => + emitOperation(nested, state.emitter, state.suspension), + ), + ...state.copy(state.slots(selected.result), state.slots(operation.destination)), + ] + return [ + Instr.localGet(state.scalar(operation.condition)), + Instr.ifElse(Instr.emptyBlockType, branch(operation.taken), branch(operation.otherwise)), + ] +} + const emitEnumConstantOperation = ( operation: Extract, state: WasmOperationContext, @@ -6127,10 +6143,12 @@ const emitReifyEffectOperation = ( return suspension?.originate(suspensionRegion) ?? [] const outcomeSlots = slots(operation.outcome) const destinationSlots = slots(operation.destination) + const successSlots = slots(operation.successValue) + const failureSlots = slots(operation.failureValue) const outcomeTag = outcomeSlots.at(0) - const resultTag = destinationSlots.at(0) - if (outcomeTag === undefined || resultTag === undefined) - throw new RangeError('Wasm Effect result lost an outcome or Result tag lane') + const valid = destinationSlots.at(0) + if (outcomeTag === undefined || valid === undefined) + throw new RangeError('Wasm Effect result lost an outcome or validity lane') const invoke = [ ...slots(operation.effect).map((slot) => Instr.localGet(slot)), ...operation.arguments.flatMap((argument) => @@ -6161,40 +6179,35 @@ const emitReifyEffectOperation = ( } return [Instr.localGet(value), ...bridgeToMember, ...bridgeToTarget, Instr.localSet(target)] }) - const resultPayload = destinationSlots.slice(1) const successLaneCount = operation.outcomeShape.tree._tag === 'OutcomeShape' ? operation.outcomeShape.tree.success.laneCount : 0 - const successShape = LayoutPlan.callingShape(plan, operation.outcomeType.type.success) - if (successShape === undefined) - throw new RangeError('Wasm Effect Result lost its success member shape') const success = [ - Instr.i32Const(operation.successTag), - Instr.localSet(resultTag), - ...writePayload(outcomeSlots.slice(1, 1 + successLaneCount), resultPayload, successShape.lanes), + Instr.i32Const(1), + Instr.localSet(valid), + ...writePayload( + outcomeSlots.slice(1, 1 + successLaneCount), + successSlots, + operation.successShape.lanes, + ), ] - let failure: ReadonlyArray - if (SilkType.failureMembers(operation.outcomeType.type).length === 0) { - failure = [Instr.op('unreachable')] + let failurePayload: ReadonlyArray + if (SilkType.isUnion(operation.failureValueType)) { + const innerTag = failureSlots.at(0) + if (innerTag === undefined) + throw new RangeError('Wasm Effect result failure lost its union tag lane') + failurePayload = [ + Instr.localGet(outcomeTag), + Instr.i32Const(1), + Instr.op('i32.sub'), + Instr.localSet(innerTag), + ...writePayload(outcomeSlots.slice(1), failureSlots.slice(1)), + ] } else { - let payload: ReadonlyArray - if (SilkType.isUnion(operation.failureValueType)) { - const innerTag = resultPayload.at(0) - if (innerTag === undefined) - throw new RangeError('Wasm Effect Result failure lost its nested tag lane') - payload = [ - Instr.localGet(outcomeTag), - Instr.i32Const(1), - Instr.op('i32.sub'), - Instr.localSet(innerTag), - ...writePayload(outcomeSlots.slice(1), resultPayload.slice(1)), - ] - } else { - payload = writePayload(outcomeSlots.slice(1), resultPayload) - } - failure = [Instr.i32Const(operation.failureTag), Instr.localSet(resultTag), ...payload] + failurePayload = writePayload(outcomeSlots.slice(1), failureSlots) } + const failure = [Instr.i32Const(0), Instr.localSet(valid), ...failurePayload] return [ ...(skipInvocation ? [] : invoke), ...(skipInvocation || suspensionRegion?._tag !== 'RunSuspendableEffectRegion' @@ -7318,6 +7331,8 @@ const emitOperationWithContext = ( return emitSlotDropOperation(operation, context) case 'ShortCircuit': return emitShortCircuitOperation(operation, context) + case 'Conditional': + return emitConditionalOperation(operation, context) case 'Match': return emitMatchOperation(operation, context) case 'Literal': diff --git a/packages/compiler/stdlib/silk/effect.silk b/packages/compiler/stdlib/silk/effect.silk index c4accc7b9..640f200d4 100644 --- a/packages/compiler/stdlib/silk/effect.silk +++ b/packages/compiler/stdlib/silk/effect.silk @@ -121,7 +121,7 @@ import silk.bool as bool import silk.logger { LogError, LogLevel, Logger } -import silk.result { Result } +import silk.result { Result, failResult, succeed } import silk.usize as usize /// The importable name of the `silk.effect` module scope. @@ -229,7 +229,7 @@ pub effect fn logError( pub effect fn result( protected: once Effect ) -> Result ? R { - return run Intrinsic.effectResult(move protected) + return run Intrinsic.effectResult>(move protected, succeed, failResult) } effect fn raise(error: E) -> never ! E { diff --git a/packages/compiler/stdlib/silk/filesystem.silk b/packages/compiler/stdlib/silk/filesystem.silk index dfde06333..3795c17d8 100644 --- a/packages/compiler/stdlib/silk/filesystem.silk +++ b/packages/compiler/stdlib/silk/filesystem.silk @@ -934,7 +934,7 @@ pub effect fn createDirectoriesRecursively( if boundary { let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index) let prefix = run finishPath(move prefixBytes) - let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix)) + let completed = run Effect.result(FileSystem.stat(&prefix)) let decision = classifyDirectory(move completed) let ensured = match move decision { DirectoryPresent {} => () @@ -978,7 +978,7 @@ pub effect fn writeFileWithParents( if boundary { let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index) let prefix = run finishPath(move prefixBytes) - let completed = run Intrinsic.effectResult(FileSystem.stat(&prefix)) + let completed = run Effect.result(FileSystem.stat(&prefix)) let decision = classifyDirectory(move completed) let ensured = match move decision { DirectoryPresent {} => () @@ -1004,7 +1004,7 @@ effect fn existsFailure(failure: FileError) -> bool ! FileError { /// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider /// failure propagate so callers cannot mistake an inaccessible path for an absent one. pub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem { - let completed = run Intrinsic.effectResult(FileSystem.stat(path)) + let completed = run Effect.result(FileSystem.stat(path)) return match move completed { Result.Success { value: info } => true Result.Failure { error: failure } => run existsFailure(move failure) diff --git a/packages/compiler/stdlib/silk/logger.silk b/packages/compiler/stdlib/silk/logger.silk index 577fea8f2..efa5be1fb 100644 --- a/packages/compiler/stdlib/silk/logger.silk +++ b/packages/compiler/stdlib/silk/logger.silk @@ -50,6 +50,7 @@ //! ``` import silk.bool as bool +import silk.effect { Effect } import silk.i32 as i32 import silk.result { Result } import silk.standard_streams { @@ -153,7 +154,7 @@ effect fn writeStdout( ) -> () ! LogError { let mut streams = nativeStreams() let bytes = stringUtf8Bytes(message) - let completed = run Intrinsic.effectResult(writeStdoutCounted(&mut streams, bytes)) + let completed = run Effect.result(writeStdoutCounted(&mut streams, bytes)) return match move completed { Result.Success { value: success } => () Result.Failure { error: failure } => run reject(3) diff --git a/packages/compiler/stdlib/silk/os_filesystem.silk b/packages/compiler/stdlib/silk/os_filesystem.silk index 09bdb59ee..851412ee8 100644 --- a/packages/compiler/stdlib/silk/os_filesystem.silk +++ b/packages/compiler/stdlib/silk/os_filesystem.silk @@ -53,6 +53,7 @@ //! ``` import silk.bool as bool +import silk.effect { Effect } import silk.bytes { Bytes, append as bytesAppend, @@ -310,8 +311,8 @@ effect fn readFile( path: &Path ) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator { let mut handle = run openFile(move self, path, 0, readFileOperation()) - let attempted = run Intrinsic.effectResult(readLoop(&mut handle)) - let closed = run Intrinsic.effectResult(close(move handle, readFileOperation())) + let attempted = run Effect.result(readLoop(&mut handle)) + let closed = run Effect.result(close(move handle, readFileOperation())) return match move attempted { Result.Success { value: bytes } => match move closed { Result<(), FileError>.Success { value: completed } => move bytes @@ -346,8 +347,8 @@ effect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError { effect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError { let mut handle = run openFile(move self, path, 1, writeFileOperation()) - let attempted = run Intrinsic.effectResult(writeLoop(&mut handle, bytes)) - let closed = run Intrinsic.effectResult(close(move handle, writeFileOperation())) + let attempted = run Effect.result(writeLoop(&mut handle, bytes)) + let closed = run Effect.result(close(move handle, writeFileOperation())) return match move attempted { Result<(), FileError>.Success { value: completed } => match move closed { Result<(), FileError>.Success { value: closedValue } => () @@ -456,8 +457,8 @@ effect fn listDirectory( path: &Path ) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator { let mut handle = run openDirectory(move self, path, listDirectoryOperation()) - let attempted = run Intrinsic.effectResult(listLoop(&mut handle, path)) - let closed = run Intrinsic.effectResult(close(move handle, listDirectoryOperation())) + let attempted = run Effect.result(listLoop(&mut handle, path)) + let closed = run Effect.result(close(move handle, listDirectoryOperation())) return match move attempted { Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed { Result<(), FileError>.Success { value: completed } => move entries From 29ec9050cf030bd326300c4b673b19fb2b39bab4 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 16:07:52 -0300 Subject: [PATCH 14/42] feat: remove optional OS count carriers --- .../compiler/src/BootstrapOsIntrinsics.ts | 77 ++-- packages/compiler/src/Intrinsic.ts | 66 ++-- packages/compiler/src/OsRuntime.ts | 144 ++++---- packages/compiler/src/Stdlib.generated.ts | 16 +- .../src/ToolchainIntegrity.generated.ts | 2 +- .../stdlib/silk/os_child_process.silk | 24 +- .../compiler/stdlib/silk/os_filesystem.silk | 78 +++-- .../compiler/stdlib/silk/os_host_input.silk | 57 +-- .../stdlib/silk/os_standard_input.silk | 21 +- packages/compiler/test/ChildProcess.test.ts | 2 +- .../test/FileSystemAcceptance.test.ts | 4 +- .../compiler/test/IntrinsicCatalog.test.ts | 54 +-- packages/compiler/test/OsFileSystem.test.ts | 6 +- .../test/TemporaryDirectoryAcceptance.test.ts | 3 +- .../test/fixtures/intrinsic-inventory.json | 328 +++++++++--------- 15 files changed, 486 insertions(+), 396 deletions(-) diff --git a/packages/compiler/src/BootstrapOsIntrinsics.ts b/packages/compiler/src/BootstrapOsIntrinsics.ts index f304982a9..21aa7be83 100644 --- a/packages/compiler/src/BootstrapOsIntrinsics.ts +++ b/packages/compiler/src/BootstrapOsIntrinsics.ts @@ -382,19 +382,22 @@ export const execute = ( const input = state.standardInput if (input === undefined) return blockedStep({ _tag: 'MissingStandardInput' }) const output = arguments_.at(0) - if (output === undefined) throw new RangeError('OS read omitted its output buffer') + const count = arguments_.at(1) + if (output === undefined || count === undefined) + throw new RangeError('OS read omitted its outputs') const capacity = byteView(output).length const result = input.read(capacity) if (result._tag === 'ReadFailure') { status({ _tag: 'Failure', reason: 'Other' }) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) break } if (result.bytes.length > capacity) throw new RangeError('standard-input provider overran the caller buffer') writeByteView(output, result.bytes) + replaceReferenced(count, integerValue('usize', BigInt(result.bytes.length))) status() - commit(optionValue('usize', integerValue('usize', BigInt(result.bytes.length)))) + commit(integerValue('i32', 1)) break } if (name === 'osProcessExecute') { @@ -488,25 +491,32 @@ export const execute = ( const stream = arguments_.at(0) const offset = arguments_.at(1) const output = arguments_.at(2) - if (stream === undefined || offset === undefined || output === undefined) + const count = arguments_.at(3) + if ( + stream === undefined || + offset === undefined || + output === undefined || + count === undefined + ) throw new RangeError('OS capture omitted arguments') const selector = readInteger(stream, 'i32').value const captured = state.processCaptures.at(Number(selector)) const start = Number(readInteger(offset, 'usize').value) if (selector !== 0n && selector !== 1n) { status({ _tag: 'Failure', reason: 'WrongType' }) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) break } if (captured === undefined || start > captured.length) { status({ _tag: 'Failure', reason: 'InvalidPath' }) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) break } const transferred = captured.slice(start, start + byteView(output).length) writeByteView(output, transferred) + replaceReferenced(count, integerValue('usize', BigInt(transferred.length))) status() - commit(optionValue('usize', integerValue('usize', BigInt(transferred.length)))) + commit(integerValue('i32', 1)) break } if (name.startsWith('osHost')) { @@ -527,7 +537,9 @@ export const execute = ( break } const output = arguments_.at(name === 'osHostWorkingDirectory' ? 0 : 1) - if (output === undefined) throw new RangeError('OS lookup omitted its output buffer') + const count = arguments_.at(name === 'osHostWorkingDirectory' ? 1 : 2) + if (output === undefined || count === undefined) + throw new RangeError('OS lookup omitted its outputs') const selector = arguments_.at(0) if (selector === undefined) throw new RangeError('OS lookup omitted its subject') let result: HostInput.Lookup @@ -545,15 +557,16 @@ export const execute = ( _tag: 'Failure', reason: result._tag === 'Absent' ? 'NotFound' : 'Other', }) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) break } // The complete byte length is the result even when only a prefix fit, so the caller // can size an exact buffer and ask again. const capacity = byteView(output).length writeByteView(output, result.bytes.slice(0, capacity)) + replaceReferenced(count, integerValue('usize', BigInt(result.bytes.length))) status() - commit(optionValue('usize', integerValue('usize', BigInt(result.bytes.length)))) + commit(integerValue('i32', 1)) break } const host = state.osFileSystem @@ -586,17 +599,19 @@ export const execute = ( if (name === 'osFileRead') { const handle = arguments_.at(0) const output = arguments_.at(1) - if (handle === undefined || output === undefined) + const count = arguments_.at(2) + if (handle === undefined || output === undefined || count === undefined) throw new RangeError('OS read omitted arguments') const capacity = byteView(output).length const result = invoke(() => host.fileRead(hostHandle(handle), capacity)) if (result._tag === 'Failure') { status(result) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) } else { writeByteView(output, result.bytes) + replaceReferenced(count, integerValue('usize', BigInt(result.bytes.length))) status() - commit(optionValue('usize', integerValue('usize', BigInt(result.bytes.length)))) + commit(integerValue('i32', 1)) } break } @@ -604,7 +619,13 @@ export const execute = ( const handle = arguments_.at(0) const input = arguments_.at(1) const offset = arguments_.at(2) - if (handle === undefined || input === undefined || offset === undefined) + const count = arguments_.at(3) + if ( + handle === undefined || + input === undefined || + offset === undefined || + count === undefined + ) throw new RangeError('OS write omitted arguments') const result = invoke(() => host.fileWrite( @@ -614,21 +635,24 @@ export const execute = ( ) if (result._tag === 'Failure') { status(result) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) } else { + replaceReferenced(count, integerValue('usize', BigInt(result.count))) status() - commit(optionValue('usize', integerValue('usize', BigInt(result.count)))) + commit(integerValue('i32', 1)) } break } if (name === 'osDirectoryNext') { const handle = arguments_.at(0) const output = arguments_.at(1) - const kind = arguments_.at(2) - const required = arguments_.at(3) + const count = arguments_.at(2) + const kind = arguments_.at(3) + const required = arguments_.at(4) if ( handle === undefined || output === undefined || + count === undefined || kind === undefined || required === undefined ) @@ -642,15 +666,17 @@ export const execute = ( status(failure) if (result._tag === 'BufferTooSmall') replaceReferenced(required, integerValue('usize', BigInt(result.requiredCapacity))) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) } else if (result._tag === 'End') { + replaceReferenced(count, integerValue('usize', 0n)) status() - commit(optionValue('usize', integerValue('usize', 0n))) + commit(integerValue('i32', 1)) } else { writeByteView(output, result.name) + replaceReferenced(count, integerValue('usize', BigInt(result.name.length))) replaceReferenced(kind, integerValue('i32', result.kind === 'File' ? 0 : 1)) status() - commit(optionValue('usize', integerValue('usize', BigInt(result.name.length)))) + commit(integerValue('i32', 1)) } break } @@ -659,12 +685,14 @@ export const execute = ( const parent = arguments_.at(1) const prefix = arguments_.at(2) const output = arguments_.at(3) - const required = arguments_.at(4) + const count = arguments_.at(4) + const required = arguments_.at(5) if ( root === undefined || parent === undefined || prefix === undefined || output === undefined || + count === undefined || required === undefined ) throw new RangeError('OS unique directory create omitted arguments') @@ -682,11 +710,12 @@ export const execute = ( ) if (result._tag === 'BufferTooSmall') replaceReferenced(required, integerValue('usize', BigInt(result.requiredCapacity))) - commit(optionValue('usize')) + commit(integerValue('i32', 0)) } else { writeByteView(output, result.name) + replaceReferenced(count, integerValue('usize', BigInt(result.name.length))) status() - commit(optionValue('usize', integerValue('usize', BigInt(result.name.length)))) + commit(integerValue('i32', 1)) } break } diff --git a/packages/compiler/src/Intrinsic.ts b/packages/compiler/src/Intrinsic.ts index 5080d1102..a045fbaa1 100644 --- a/packages/compiler/src/Intrinsic.ts +++ b/packages/compiler/src/Intrinsic.ts @@ -896,17 +896,19 @@ const intrinsicOperations = Object.freeze([ parameters: Object.freeze([ valueParameter('handle', '&mut OsHandle'), valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), semanticParameters: Object.freeze([ mutableHandle, Type.slice('Exclusive', 'u8'), + mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'handle is a live file; output is initialized writable storage; success reports the exact transferred byte count', }), @@ -917,6 +919,7 @@ const intrinsicOperations = Object.freeze([ valueParameter('handle', '&mut OsHandle'), valueParameter('input', '&[u8]'), valueParameter('offset', 'usize'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), @@ -924,11 +927,12 @@ const intrinsicOperations = Object.freeze([ mutableHandle, byteSlice, 'usize', + mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'handle is a live file; input is initialized; success reports the exact transferred byte count and may be partial', }), @@ -952,6 +956,7 @@ const intrinsicOperations = Object.freeze([ parameters: Object.freeze([ valueParameter('handle', '&mut OsHandle'), valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('kind', '&mut i32'), valueParameter('requiredCapacity', '&mut usize'), valueParameter('reason', '&mut i32'), @@ -960,13 +965,14 @@ const intrinsicOperations = Object.freeze([ semanticParameters: Object.freeze([ mutableHandle, Type.slice('Exclusive', 'u8'), + mutableUsize, mutableI32, mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'handle is a live directory; buffer-too-small does not advance and reports required capacity; zero means end', }), @@ -1002,6 +1008,7 @@ const intrinsicOperations = Object.freeze([ valueParameter('parent', '&[u8]'), valueParameter('prefix', '&[u8]'), valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('requiredCapacity', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), @@ -1012,11 +1019,12 @@ const intrinsicOperations = Object.freeze([ byteSlice, Type.slice('Exclusive', 'u8'), mutableUsize, + mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'root and parent satisfy confined traversal; prefix is one valid final component fragment; the provider chooses the unique suffix, creates exactly one directory no other caller holds, and writes its complete final component name; buffer-too-small creates nothing and reports required capacity', }), @@ -1061,12 +1069,18 @@ const intrinsicOperations = Object.freeze([ operation: 'OsStandardInputRead', parameters: Object.freeze([ valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), - semanticParameters: Object.freeze([Type.slice('Exclusive', 'u8'), mutableI32, mutableU32]), - result: 'Effect>', - semanticResult: Type.option('usize'), + semanticParameters: Object.freeze([ + Type.slice('Exclusive', 'u8'), + mutableUsize, + mutableI32, + mutableU32, + ]), + result: 'Effect', + semanticResult: 'bool', invariant: 'output is initialized writable storage; success reports the exact transferred byte count and zero means end of input', }), @@ -1109,6 +1123,7 @@ const intrinsicOperations = Object.freeze([ valueParameter('stream', 'i32'), valueParameter('offset', 'usize'), valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), @@ -1116,11 +1131,12 @@ const intrinsicOperations = Object.freeze([ 'i32', 'usize', Type.slice('Exclusive', 'u8'), + mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'stream selects zero for standard output or one for standard error, offset is within the retained capture of the immediately preceding execute, and output is initialized writable storage', }), @@ -1143,17 +1159,19 @@ const intrinsicOperations = Object.freeze([ parameters: Object.freeze([ valueParameter('index', 'usize'), valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), semanticParameters: Object.freeze([ 'usize', Type.slice('Exclusive', 'u8'), + mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'output is initialized writable storage; success reports the complete argument byte length and copies the prefix that fits, and absence reports the not-found reason', }), @@ -1163,17 +1181,19 @@ const intrinsicOperations = Object.freeze([ parameters: Object.freeze([ valueParameter('name', '&[u8]'), valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), semanticParameters: Object.freeze([ byteSlice, Type.slice('Exclusive', 'u8'), + mutableUsize, mutableI32, mutableU32, ]), - result: 'Effect>', - semanticResult: Type.option('usize'), + result: 'Effect', + semanticResult: 'bool', invariant: 'output is initialized writable storage; success reports the complete value byte length and copies the prefix that fits, and an unset name reports the not-found reason', }), @@ -1182,12 +1202,18 @@ const intrinsicOperations = Object.freeze([ operation: 'OsHostWorkingDirectory', parameters: Object.freeze([ valueParameter('output', '&mut [u8]'), + valueParameter('count', '&mut usize'), valueParameter('reason', '&mut i32'), valueParameter('nativeCode', '&mut u32'), ]), - semanticParameters: Object.freeze([Type.slice('Exclusive', 'u8'), mutableI32, mutableU32]), - result: 'Effect>', - semanticResult: Type.option('usize'), + semanticParameters: Object.freeze([ + Type.slice('Exclusive', 'u8'), + mutableUsize, + mutableI32, + mutableU32, + ]), + result: 'Effect', + semanticResult: 'bool', invariant: 'output is initialized writable storage; success reports the complete working-directory byte length and copies the prefix that fits', }), diff --git a/packages/compiler/src/OsRuntime.ts b/packages/compiler/src/OsRuntime.ts index 94335dce1..c8bf83c4f 100644 --- a/packages/compiler/src/OsRuntime.ts +++ b/packages/compiler/src/OsRuntime.ts @@ -46,8 +46,6 @@ enum { SILK_OTHER = 10 }; -typedef struct { int tag; size_t value; } silk_option_usize; - static int silk_reason_from_errno(int value) { switch (value) { case ENOENT: return SILK_NOT_FOUND; @@ -85,14 +83,9 @@ static void silk_success(int *reason, uint32_t *native_code) { *native_code = 0; } -static silk_option_usize silk_transfer(size_t count) { - silk_option_usize result = { 1, count }; - return result; -} - -static silk_option_usize silk_transfer_failure(void) { - silk_option_usize result = { 0, 0 }; - return result; +static int silk_transfer(size_t count, size_t *output) { + *output = count; + return 1; } ` @@ -387,19 +380,19 @@ extern char **environ; /* Copies the prefix of one host value that fits and reports the value's complete byte length, so a caller that received a short buffer can size an exact one and ask again. */ -static silk_option_usize silk_host_copy(const unsigned char *value, size_t length, - unsigned char *output, size_t capacity, - int *reason, uint32_t *native_code) { +static int silk_host_copy(const unsigned char *value, size_t length, + unsigned char *output, size_t capacity, size_t *count, + int *reason, uint32_t *native_code) { size_t committed = length < capacity ? length : capacity; if (committed > 0) memcpy(output, value, committed); silk_success(reason, native_code); - return silk_transfer(length); + return silk_transfer(length, count); } /* An absent value: an index past the last argument, or an unset variable name. */ -static silk_option_usize silk_host_absent(int *reason, uint32_t *native_code) { +static int silk_host_absent(int *reason, uint32_t *native_code) { silk_protocol_failure(reason, native_code, SILK_NOT_FOUND); - return silk_transfer_failure(); + return 0; } ` @@ -497,29 +490,29 @@ int silk_os_file_open_v1(const unsigned char *root, size_t root_length, } `, silk_os_file_read_v1: ` -silk_option_usize silk_os_file_read_v1(silk_os_handle *handle, unsigned char *output, - size_t capacity, int *reason, uint32_t *native_code) { +int silk_os_file_read_v1(silk_os_handle *handle, unsigned char *output, + size_t capacity, size_t *count, int *reason, uint32_t *native_code) { silk_native_handle *native = silk_live(handle, 0, reason, native_code); - if (native == NULL) return silk_transfer_failure(); + if (native == NULL) return 0; ssize_t received; do { received = read(native->fd, output, capacity); } while (received < 0 && errno == EINTR); - if (received < 0) { silk_failure(reason, native_code, errno); return silk_transfer_failure(); } + if (received < 0) { silk_failure(reason, native_code, errno); return 0; } silk_success(reason, native_code); - return silk_transfer((size_t)received); + return silk_transfer((size_t)received, count); } `, silk_os_file_write_v1: ` -silk_option_usize silk_os_file_write_v1(silk_os_handle *handle, const unsigned char *input, - size_t length, size_t offset, - int *reason, uint32_t *native_code) { +int silk_os_file_write_v1(silk_os_handle *handle, const unsigned char *input, + size_t length, size_t offset, size_t *count, + int *reason, uint32_t *native_code) { silk_native_handle *native = silk_live(handle, 0, reason, native_code); - if (native == NULL) return silk_transfer_failure(); - if (offset > length) { silk_protocol_failure(reason, native_code, SILK_INVALID_PATH); return silk_transfer_failure(); } + if (native == NULL) return 0; + if (offset > length) { silk_protocol_failure(reason, native_code, SILK_INVALID_PATH); return 0; } ssize_t written; do { written = write(native->fd, input + offset, length - offset); } while (written < 0 && errno == EINTR); - if (written < 0) { silk_failure(reason, native_code, errno); return silk_transfer_failure(); } + if (written < 0) { silk_failure(reason, native_code, errno); return 0; } silk_success(reason, native_code); - return silk_transfer((size_t)written); + return silk_transfer((size_t)written, count); } `, silk_os_directory_open_v1: ` @@ -545,34 +538,34 @@ int silk_os_directory_open_v1(const unsigned char *root, size_t root_length, } `, silk_os_directory_next_v1: ` -silk_option_usize silk_os_directory_next_v1(silk_os_handle *handle, unsigned char *output, - size_t capacity, int *kind, size_t *required, - int *reason, uint32_t *native_code) { +int silk_os_directory_next_v1(silk_os_handle *handle, unsigned char *output, + size_t capacity, size_t *count, int *kind, size_t *required, + int *reason, uint32_t *native_code) { silk_native_handle *native = silk_live(handle, 1, reason, native_code); - if (native == NULL) return silk_transfer_failure(); + if (native == NULL) return 0; while (native->pending_name == NULL) { errno = 0; struct dirent *entry = readdir(native->directory); if (entry == NULL) { - if (errno != 0) { silk_failure(reason, native_code, errno); return silk_transfer_failure(); } + if (errno != 0) { silk_failure(reason, native_code, errno); return 0; } silk_success(reason, native_code); - return silk_transfer(0); + return silk_transfer(0, count); } if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; struct stat info; if (fstatat(native->fd, entry->d_name, &info, AT_SYMLINK_NOFOLLOW) != 0) { silk_failure(reason, native_code, errno); - return silk_transfer_failure(); + return 0; } if (S_ISLNK(info.st_mode) || (!S_ISREG(info.st_mode) && !S_ISDIR(info.st_mode))) { silk_protocol_failure(reason, native_code, SILK_WRONG_TYPE); - return silk_transfer_failure(); + return 0; } native->pending_length = strlen(entry->d_name); native->pending_name = (unsigned char *)malloc(native->pending_length); if (native->pending_name == NULL) { silk_protocol_failure(reason, native_code, SILK_NO_SPACE); - return silk_transfer_failure(); + return 0; } memcpy(native->pending_name, entry->d_name, native->pending_length); native->pending_kind = S_ISREG(info.st_mode) ? 0 : 1; @@ -580,7 +573,7 @@ silk_option_usize silk_os_directory_next_v1(silk_os_handle *handle, unsigned cha if (capacity < native->pending_length) { *required = native->pending_length; silk_protocol_failure(reason, native_code, SILK_BUFFER_TOO_SMALL); - return silk_transfer_failure(); + return 0; } memcpy(output, native->pending_name, native->pending_length); *kind = native->pending_kind; @@ -589,7 +582,7 @@ silk_option_usize silk_os_directory_next_v1(silk_os_handle *handle, unsigned cha native->pending_name = NULL; native->pending_length = 0; silk_success(reason, native_code); - return silk_transfer(length); + return silk_transfer(length, count); } `, silk_os_path_inspect_v1: ` @@ -628,32 +621,30 @@ int silk_os_directory_create_v1(const unsigned char *root, size_t root_length, } `, silk_os_directory_create_unique_v1: ` -silk_option_usize silk_os_directory_create_unique_v1(const unsigned char *root, size_t root_length, - const unsigned char *parent, - size_t parent_length, - const unsigned char *prefix, - size_t prefix_length, unsigned char *output, - size_t capacity, size_t *required, - int *reason, uint32_t *native_code) { +int silk_os_directory_create_unique_v1(const unsigned char *root, size_t root_length, + const unsigned char *parent, size_t parent_length, + const unsigned char *prefix, size_t prefix_length, + unsigned char *output, size_t capacity, size_t *count, + size_t *required, int *reason, uint32_t *native_code) { static const char alphabet[] = "abcdefghijklmnopqrstuvwxyz0123456789"; size_t length = prefix_length + SILK_UNIQUE_SUFFIX; if (capacity < length) { *required = length; silk_protocol_failure(reason, native_code, SILK_BUFFER_TOO_SMALL); - return silk_transfer_failure(); + return 0; } if (!silk_utf8(prefix, prefix_length) || !silk_component_valid(prefix, prefix_length) || memchr(prefix, '/', prefix_length) != NULL) { silk_protocol_failure(reason, native_code, SILK_INVALID_PATH); - return silk_transfer_failure(); + return 0; } int directory = silk_directory(root, root_length, parent, parent_length, reason, native_code); - if (directory < 0) return silk_transfer_failure(); + if (directory < 0) return 0; char *name = (char *)malloc(length + 1); if (name == NULL) { close(directory); silk_protocol_failure(reason, native_code, SILK_NO_SPACE); - return silk_transfer_failure(); + return 0; } memcpy(name, prefix, prefix_length); name[length] = 0; @@ -669,20 +660,20 @@ silk_option_usize silk_os_directory_create_unique_v1(const unsigned char *root, free(name); close(directory); silk_success(reason, native_code); - return silk_transfer(length); + return silk_transfer(length, count); } int selected = errno; if (selected != EEXIST) { free(name); close(directory); silk_failure(reason, native_code, selected); - return silk_transfer_failure(); + return 0; } } free(name); close(directory); silk_protocol_failure(reason, native_code, SILK_ALREADY_EXISTS); - return silk_transfer_failure(); + return 0; } `, silk_os_file_remove_v1: ` @@ -739,13 +730,13 @@ int silk_os_handle_close_v1(size_t identity, int kind, int active, } `, silk_os_standard_input_read_v1: ` -silk_option_usize silk_os_standard_input_read_v1(unsigned char *output, size_t capacity, - int *reason, uint32_t *native_code) { +int silk_os_standard_input_read_v1(unsigned char *output, size_t capacity, size_t *count, + int *reason, uint32_t *native_code) { ssize_t received; do { received = read(0, output, capacity); } while (received < 0 && errno == EINTR); - if (received < 0) { silk_failure(reason, native_code, errno); return silk_transfer_failure(); } + if (received < 0) { silk_failure(reason, native_code, errno); return 0; } silk_success(reason, native_code); - return silk_transfer((size_t)received); + return silk_transfer((size_t)received, count); } `, silk_os_process_execute_v1: ` @@ -914,22 +905,23 @@ int silk_os_process_execute_v1(const unsigned char *program, size_t program_leng } `, silk_os_process_capture_v1: ` -silk_option_usize silk_os_process_capture_v1(int stream, size_t offset, unsigned char *output, - size_t capacity, int *reason, uint32_t *native_code) { +int silk_os_process_capture_v1(int stream, size_t offset, unsigned char *output, + size_t capacity, size_t *count, + int *reason, uint32_t *native_code) { if (stream != 0 && stream != 1) { silk_protocol_failure(reason, native_code, SILK_WRONG_TYPE); - return silk_transfer_failure(); + return 0; } silk_capture *capture = &silk_captures[stream]; if (offset > capture->length) { silk_protocol_failure(reason, native_code, SILK_INVALID_PATH); - return silk_transfer_failure(); + return 0; } size_t remaining = capture->length - offset; size_t transferred = remaining < capacity ? remaining : capacity; if (transferred != 0) memcpy(output, capture->bytes + offset, transferred); silk_success(reason, native_code); - return silk_transfer(transferred); + return silk_transfer(transferred, count); } `, silk_os_host_argument_count_v1: ` @@ -944,22 +936,22 @@ int silk_os_host_argument_count_v1(size_t *count, int *reason, uint32_t *native_ } `, silk_os_host_argument_v1: ` -silk_option_usize silk_os_host_argument_v1(size_t index, unsigned char *output, size_t capacity, - int *reason, uint32_t *native_code) { +int silk_os_host_argument_v1(size_t index, unsigned char *output, size_t capacity, size_t *count, + int *reason, uint32_t *native_code) { if (silk_host_argv_v1 == NULL || silk_host_argc_v1 < 0 || index >= (size_t)silk_host_argc_v1) { return silk_host_absent(reason, native_code); } const char *selected = silk_host_argv_v1[index]; if (selected == NULL) return silk_host_absent(reason, native_code); - return silk_host_copy((const unsigned char *)selected, strlen(selected), output, capacity, + return silk_host_copy((const unsigned char *)selected, strlen(selected), output, capacity, count, reason, native_code); } `, silk_os_host_variable_v1: ` -silk_option_usize silk_os_host_variable_v1(const unsigned char *name, size_t name_length, - unsigned char *output, size_t capacity, - int *reason, uint32_t *native_code) { +int silk_os_host_variable_v1(const unsigned char *name, size_t name_length, + unsigned char *output, size_t capacity, size_t *count, + int *reason, uint32_t *native_code) { char **entries = silk_host_environ; if (entries == NULL) return silk_host_absent(reason, native_code); /* The environment block is scanned by raw bytes rather than through getenv, so a name or value @@ -971,25 +963,25 @@ silk_option_usize silk_os_host_variable_v1(const unsigned char *name, size_t nam if ((size_t)(separator - text) != name_length) continue; if (name_length > 0 && memcmp(text, name, name_length) != 0) continue; const unsigned char *value = separator + 1; - return silk_host_copy(value, strlen((const char *)value), output, capacity, reason, + return silk_host_copy(value, strlen((const char *)value), output, capacity, count, reason, native_code); } return silk_host_absent(reason, native_code); } `, silk_os_host_working_directory_v1: ` -silk_option_usize silk_os_host_working_directory_v1(unsigned char *output, size_t capacity, - int *reason, uint32_t *native_code) { +int silk_os_host_working_directory_v1(unsigned char *output, size_t capacity, size_t *count, + int *reason, uint32_t *native_code) { size_t room = 256; while (room <= ((size_t)1 << 20)) { char *buffer = (char *)malloc(room); if (buffer == NULL) { silk_protocol_failure(reason, native_code, SILK_NO_SPACE); - return silk_transfer_failure(); + return 0; } if (getcwd(buffer, room) != NULL) { - silk_option_usize result = silk_host_copy((const unsigned char *)buffer, strlen(buffer), - output, capacity, reason, native_code); + int result = silk_host_copy((const unsigned char *)buffer, strlen(buffer), output, capacity, + count, reason, native_code); free(buffer); return result; } @@ -997,12 +989,12 @@ silk_option_usize silk_os_host_working_directory_v1(unsigned char *output, size_ free(buffer); if (selected != ERANGE) { silk_failure(reason, native_code, selected); - return silk_transfer_failure(); + return 0; } room *= 2; } silk_protocol_failure(reason, native_code, SILK_TOO_LARGE); - return silk_transfer_failure(); + return 0; } `, silk_os_system_clock_now_v1: ` diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index daa3a5819..fbec6134d 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -830,20 +830,20 @@ export const modules = [ module: 'silk/os_child_process', path: 'silk/os_child_process.silk', sourceIdentity: 'silk/os_child_process', - digest: 'ed1e0a678f9724f34820e697b9eb95ab3154d223e03192089228e46e3c868c2e', + digest: '5a53a35977e6ba3d4fa8953a669b1f698c485d394cf3974abb9440bd3c2d5ed0', documentation: 'silk/os_child_process.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], runtimeInventory: ['osProcessCapture', 'osProcessExecute'], namespace: 'OsChildProcess', source: - '//! Native [`ChildProcess`] provider that executes directly through the platform process boundary.\n//!\n//! # When to use\n//! Construct [`OsChildProcess`] at a native application edge, then provide it to portable code that\n//! requires [`ChildProcess`]. Tests can supply an in-memory provider without importing this module.\n//!\n//! # Details\n//! The provider owns no persistent state. It translates low-level spawn and capture reasons into\n//! portable [`ProcessError`] data, preserves a native numeric code, and copies the complete stdout\n//! and stderr captures into independently owned [`Bytes`] values. Exit status and signal\n//! termination remain ordinary [`ProcessOutcome`] data.\n//!\n//! Constructing the provider performs no process operation. Portable code invokes\n//! `ChildProcess.execute` after an application provides `&mut OsChildProcess` for the\n//! `&mut ChildProcess` requirement and supplies an allocator for owned captures.\n//!\n//! # Gotchas\n//! Reachable OS process operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing a process host import; evaluator execution requires an injected host\n//! adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without starting a process\n//!\n//! ```silk\n//! import silk.os_child_process as OsChildProcess\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsChildProcess.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.child_process {\n ChildProcess,\n ProcessError,\n ProcessOperation,\n ProcessOutcome,\n ProcessReason,\n ProcessRequest,\n arguments as requestArguments,\n captureOperation,\n environment as requestEnvironment,\n exited,\n failureWithCode,\n invalidRequest,\n noSpace,\n notFound,\n other,\n permissionDenied,\n program as requestProgram,\n signaled,\n spawnOperation,\n unsupported,\n workingDirectory as requestWorkingDirectory\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.i32 as i32\nimport silk.option { Option, none }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`ChildProcess`] provider with blocking execution and complete capture.\n///\n/// # Details\n///\n/// The provider borrows the request and transfers each completed capture into independent [`Bytes`]\n/// storage. The returned [`ProcessOutcome`] owns that storage.\npub struct OsChildProcess {}\n\n/// Creates a stateless provider for the native process boundary.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut ChildProcess` to\n/// portable code that calls `ChildProcess.execute` or `silk.child_process.submit`.\n///\n/// # Details\n///\n/// Construction starts no process and allocates no storage. Each execution translates native\n/// failures into [`ProcessError`] and requires an allocator for owned output captures.\npub fn make() -> OsChildProcess {\n return OsChildProcess {}\n}\n\nfn reason(value: i32) -> ProcessReason {\n if value == 0 { return notFound() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidRequest() }\n if value == 4 { return invalidRequest() }\n if value == 6 { return noSpace() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(\n operation: ProcessOperation,\n lowReason: i32,\n nativeCode: u32\n) -> never ! ProcessError {\n fail failureWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawExecute(\n program: &[u8],\n arguments: &[u8],\n environment: &[u8],\n workingDirectory: &[u8],\n status: &mut i32,\n code: &mut i32,\n outputLength: &mut usize,\n errorLength: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osProcessExecute(\n program,\n arguments,\n environment,\n workingDirectory,\n status,\n code,\n outputLength,\n errorLength,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\neffect fn rawCapture(\n stream: i32,\n offset: usize,\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osProcessCapture(stream, offset, output, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Copies one completed capture out of the boundary into independently owned bytes.\n///\n/// The preceding execute reported the exact length, so the outcome owns every captured byte or the\n/// capture stage fails; a short transfer that never advances is a capture failure rather than a\n/// silently truncated result.\neffect fn drain(stream: i32, length: usize) -> Bytes ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n if length == usize.ZERO { return move result }\n let mut buffer = run bytesZeroed(length)\n let mut offset = usize.ZERO\n while offset < length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let transferred = run rawCapture(stream, offset, move output, &mut lowReason, &mut nativeCode)\n let received = match move transferred {\n Option.None => run raise(captureOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if received == usize.ZERO { return run raise(captureOperation(), 10, u32.toU32(0)) }\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < received {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n offset = offset + received\n }\n return move result\n}\n\n/// Runs one request to completion and owns everything the child wrote.\n///\n/// The boundary reports termination as a status selector plus a code, which becomes `Exited` or\n/// `Signaled` here. A nonzero exit code is neither: it is data inside `Exited`.\neffect fn execute(\n self: &mut OsChildProcess,\n request: &ProcessRequest\n) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut status = 0\n let mut code = 0\n let mut outputLength = usize.ZERO\n let mut errorLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let started = run rawExecute(\n requestProgram(request),\n requestArguments(request),\n requestEnvironment(request),\n requestWorkingDirectory(request),\n &mut status,\n &mut code,\n &mut outputLength,\n &mut errorLength,\n &mut lowReason,\n &mut nativeCode\n )\n if started == false { return run raise(spawnOperation(), lowReason, nativeCode) }\n let output = run drain(0, outputLength)\n let errors = run drain(1, errorLength)\n if status == 0 { return exited(code, move output, move errors) }\n return signaled(code, move output, move errors)\n}\n\nimpl ChildProcess for OsChildProcess {\n execute: OsChildProcess.execute\n}\n', + '//! Native [`ChildProcess`] provider that executes directly through the platform process boundary.\n//!\n//! # When to use\n//! Construct [`OsChildProcess`] at a native application edge, then provide it to portable code that\n//! requires [`ChildProcess`]. Tests can supply an in-memory provider without importing this module.\n//!\n//! # Details\n//! The provider owns no persistent state. It translates low-level spawn and capture reasons into\n//! portable [`ProcessError`] data, preserves a native numeric code, and copies the complete stdout\n//! and stderr captures into independently owned [`Bytes`] values. Exit status and signal\n//! termination remain ordinary [`ProcessOutcome`] data.\n//!\n//! Constructing the provider performs no process operation. Portable code invokes\n//! `ChildProcess.execute` after an application provides `&mut OsChildProcess` for the\n//! `&mut ChildProcess` requirement and supplies an allocator for owned captures.\n//!\n//! # Gotchas\n//! Reachable OS process operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing a process host import; evaluator execution requires an injected host\n//! adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without starting a process\n//!\n//! ```silk\n//! import silk.os_child_process as OsChildProcess\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsChildProcess.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.child_process {\n ChildProcess,\n ProcessError,\n ProcessOperation,\n ProcessOutcome,\n ProcessReason,\n ProcessRequest,\n arguments as requestArguments,\n captureOperation,\n environment as requestEnvironment,\n exited,\n failureWithCode,\n invalidRequest,\n noSpace,\n notFound,\n other,\n permissionDenied,\n program as requestProgram,\n signaled,\n spawnOperation,\n unsupported,\n workingDirectory as requestWorkingDirectory\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.i32 as i32\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`ChildProcess`] provider with blocking execution and complete capture.\n///\n/// # Details\n///\n/// The provider borrows the request and transfers each completed capture into independent [`Bytes`]\n/// storage. The returned [`ProcessOutcome`] owns that storage.\npub struct OsChildProcess {}\n\n/// Creates a stateless provider for the native process boundary.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut ChildProcess` to\n/// portable code that calls `ChildProcess.execute` or `silk.child_process.submit`.\n///\n/// # Details\n///\n/// Construction starts no process and allocates no storage. Each execution translates native\n/// failures into [`ProcessError`] and requires an allocator for owned output captures.\npub fn make() -> OsChildProcess {\n return OsChildProcess {}\n}\n\nfn reason(value: i32) -> ProcessReason {\n if value == 0 { return notFound() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidRequest() }\n if value == 4 { return invalidRequest() }\n if value == 6 { return noSpace() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(\n operation: ProcessOperation,\n lowReason: i32,\n nativeCode: u32\n) -> never ! ProcessError {\n fail failureWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawExecute(\n program: &[u8],\n arguments: &[u8],\n environment: &[u8],\n workingDirectory: &[u8],\n status: &mut i32,\n code: &mut i32,\n outputLength: &mut usize,\n errorLength: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osProcessExecute(\n program,\n arguments,\n environment,\n workingDirectory,\n status,\n code,\n outputLength,\n errorLength,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\neffect fn rawCapture(\n stream: i32,\n offset: usize,\n output: &mut [u8],\n count: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osProcessCapture(stream, offset, output, count, lowReason, nativeCode)\n }\n return false\n}\n\n/// Copies one completed capture out of the boundary into independently owned bytes.\n///\n/// The preceding execute reported the exact length, so the outcome owns every captured byte or the\n/// capture stage fails; a short transfer that never advances is a capture failure rather than a\n/// silently truncated result.\neffect fn drain(stream: i32, length: usize) -> Bytes ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n if length == usize.ZERO { return move result }\n let mut buffer = run bytesZeroed(length)\n let mut offset = usize.ZERO\n while offset < length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut received = usize.ZERO\n let transferred = run rawCapture(\n stream,\n offset,\n move output,\n &mut received,\n &mut lowReason,\n &mut nativeCode\n )\n if transferred == false { return run raise(captureOperation(), lowReason, nativeCode) }\n if received == usize.ZERO { return run raise(captureOperation(), 10, u32.toU32(0)) }\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < received {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n offset = offset + received\n }\n return move result\n}\n\n/// Runs one request to completion and owns everything the child wrote.\n///\n/// The boundary reports termination as a status selector plus a code, which becomes `Exited` or\n/// `Signaled` here. A nonzero exit code is neither: it is data inside `Exited`.\neffect fn execute(\n self: &mut OsChildProcess,\n request: &ProcessRequest\n) -> ProcessOutcome ! ProcessError | OutOfMemoryError ? &mut Allocator {\n let mut status = 0\n let mut code = 0\n let mut outputLength = usize.ZERO\n let mut errorLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let started = run rawExecute(\n requestProgram(request),\n requestArguments(request),\n requestEnvironment(request),\n requestWorkingDirectory(request),\n &mut status,\n &mut code,\n &mut outputLength,\n &mut errorLength,\n &mut lowReason,\n &mut nativeCode\n )\n if started == false { return run raise(spawnOperation(), lowReason, nativeCode) }\n let output = run drain(0, outputLength)\n let errors = run drain(1, errorLength)\n if status == 0 { return exited(code, move output, move errors) }\n return signaled(code, move output, move errors)\n}\n\nimpl ChildProcess for OsChildProcess {\n execute: OsChildProcess.execute\n}\n', }, { module: 'silk/os_filesystem', path: 'silk/os_filesystem.silk', sourceIdentity: 'silk/os_filesystem', - digest: 'fff840f77d16835ec8ee4fc925fc3d95e1d71fd748a441e548faf9c259c1d285', + digest: '23a3e3878b10a6c1bda418342569af8b65356c8360df093a01349a844cff5f09', documentation: 'silk/os_filesystem.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], @@ -862,13 +862,13 @@ export const modules = [ ], namespace: 'OsFileSystem', source: - '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.effect { Effect }\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut received = none()\n unsafe {\n received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode)\n }\n let length = match move received {\n Option.None => run raise(readFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Effect.result(readLoop(&mut handle))\n let closed = run Effect.result(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut written = none()\n unsafe {\n written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode)\n }\n let length = match move written {\n Option.None => run raise(writeFileOperation(), lowReason, nativeCode)\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Effect.result(writeLoop(&mut handle, bytes))\n let closed = run Effect.result(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut next = none()\n unsafe {\n next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode)\n }\n let encodedLength = match move next {\n Option.Some { value: presentLength } => presentLength + usize.ONE\n Option.None => usize.ZERO\n }\n if encodedLength == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let length = encodedLength - usize.ONE\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Effect.result(listLoop(&mut handle, path))\n let closed = run Effect.result(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, output, required, lowReason, nativeCode)\n }\n let impossible = 1 / 0\n return none()\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n let length = match move chosen {\n Option.Some { value: selected } => selected\n Option.None => usize.ZERO\n }\n if length == usize.ZERO {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', + '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.effect { Effect }\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let mut received = false\n unsafe {\n received = run Intrinsic.osFileRead(\n handle,\n output,\n &mut length,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if received == false { return run raise(readFileOperation(), lowReason, nativeCode) }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Effect.result(readLoop(&mut handle))\n let closed = run Effect.result(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut length = usize.ZERO\n let mut written = false\n unsafe {\n written = run Intrinsic.osFileWrite(\n handle,\n bytes,\n offset,\n &mut length,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if written == false { return run raise(writeFileOperation(), lowReason, nativeCode) }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Effect.result(writeLoop(&mut handle, bytes))\n let closed = run Effect.result(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let mut next = false\n unsafe {\n next = run Intrinsic.osDirectoryNext(\n handle,\n output,\n &mut length,\n &mut kind,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if next == false {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Effect.result(listLoop(&mut handle, path))\n let closed = run Effect.result(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n count: &mut usize,\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(\n root,\n parent,\n prefix,\n output,\n count,\n required,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut length,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n if chosen == false {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', }, { module: 'silk/os_host_input', path: 'silk/os_host_input.silk', sourceIdentity: 'silk/os_host_input', - digest: '97893ddafd1e7a8575a67c8eebe8acb4923db595918153e2d85558bcac46c2aa', + digest: 'cf31680a8793fcc4eebfe1339ce63494b3739cd4ee971d0f2cc969350822689f', documentation: 'silk/os_host_input.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], @@ -880,7 +880,7 @@ export const modules = [ ], namespace: 'OsHostInput', source: - '//! Native [`HostInput`] provider for the process command line, environment, and working directory.\n//!\n//! # When to use\n//! Construct [`OsHostInput`] at a native application edge and provide it to portable code requiring\n//! [`HostInput`]. Tests can replace it with a deterministic provider and keep process state out of\n//! the program under test.\n//!\n//! # Details\n//! The provider owns no persistent state. Each successful lookup copies the host value into\n//! independent [`Bytes`], beginning with a bounded buffer and retrying once at the exact size the\n//! boundary reports. An absent argument or variable remains [`None`]; an unavailable working\n//! directory and contradictory host lengths become [`HostInputError`].\n//!\n//! Constructing the provider reads no host state. Portable code performs lookups after the\n//! application supplies `&mut OsHostInput` for the `&mut HostInput` requirement. Each owned result\n//! also requires an allocator.\n//!\n//! # Gotchas\n//! Reachable OS host-input operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing process-global imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without reading process state\n//!\n//! ```silk\n//! import silk.os_host_input as OsHostInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsHostInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n length as bytesLength,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.host_input { HostInput, HostInputError, inputFailure }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`HostInput`] provider for process arguments, environment, and directory.\n///\n/// # Details\n///\n/// The process owns the source values. Each successful byte lookup returns a new owned copy through\n/// the portable service.\npub struct OsHostInput {}\n\n/// Creates a stateless provider for native process input.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut HostInput` to\n/// portable lookup operations in `silk.host_input`.\n///\n/// # Details\n///\n/// Construction performs no lookup and cannot fail. Argument and environment absence remain\n/// ordinary `None` values when a later lookup runs.\npub fn make() -> OsHostInput {\n return OsHostInput {}\n}\n\neffect fn raise() -> never ! HostInputError {\n fail inputFailure()\n}\n\neffect fn rawArgumentCount(count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHostArgumentCount(count, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawArgument(\n index: usize,\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostArgument(index, output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawVariable(\n name: &[u8],\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostVariable(name, output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawWorkingDirectory(\n output: &mut [u8],\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> Option {\n unsafe { return run Intrinsic.osHostWorkingDirectory(output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn ownedPrefix(view: &[u8], length: usize) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\n/// Copies one host value into independently owned bytes, growing the buffer once when needed.\n///\n/// The low-level boundary reports the value\'s complete byte length even when the buffer it received\n/// was too small, so a second pass with an exactly sized buffer always completes. An absent value\n/// is `None` with the not-found reason; any other reason is a host error.\neffect fn fetch(\n selector: i32,\n index: usize,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let initial = bytesZeroed(128)\n let mut buffer = run initial\n let mut result = none()\n let mut complete = false\n let mut grown = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut received = none()\n if selector == 0 {\n received = run rawArgument(index, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n received = run rawVariable(name, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n } else {\n received = run rawWorkingDirectory(bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode)\n }\n }\n let encoded = match move received {\n Option.Some { value: total } => total + usize.ONE\n Option.None => usize.ZERO\n }\n if encoded == usize.ZERO {\n if lowReason != 0 { return run raise() }\n complete = true\n } else {\n let total = encoded - usize.ONE\n if total <= bytesLength(&buffer) {\n let owned = run ownedPrefix(bytesSlice(&buffer), total)\n result = some(move owned)\n complete = true\n } else {\n // An exactly sized buffer completes an honest host in one more pass, so a second short\n // answer means the host contradicted the length it just reported.\n if grown { return run raise() }\n grown = true\n let resized = bytesZeroed(total)\n buffer = run resized\n }\n }\n }\n return move result\n}\n\n/// Reports how many arguments the process received, including the program name at index zero.\neffect fn argumentCount(self: &mut OsHostInput) -> usize ! HostInputError {\n let mut count = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let answered = run rawArgumentCount(&mut count, &mut lowReason, &mut nativeCode)\n if answered == false { return run raise() }\n return count\n}\n\n/// Copies one argument\'s raw bytes, exactly as the process received them.\neffect fn argument(\n self: &mut OsHostInput,\n index: usize\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(0, index, stringUtf8Bytes(""))\n}\n\n/// Copies one environment value\'s raw bytes. An unset name is `None` rather than a failure.\neffect fn variable(\n self: &mut OsHostInput,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(1, usize.ZERO, name)\n}\n\n/// Copies the process working directory\'s raw bytes. It always exists, so absence is a host error.\neffect fn workingDirectory(\n self: &mut OsHostInput\n) -> Bytes ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let found = run fetch(2, usize.ZERO, stringUtf8Bytes(""))\n return match move found {\n Option.Some { value: bytes } => move bytes\n Option.None => run raise()\n }\n}\n\nimpl HostInput for OsHostInput {\n argumentCount: OsHostInput.argumentCount\n argument: OsHostInput.argument\n variable: OsHostInput.variable\n workingDirectory: OsHostInput.workingDirectory\n}\n', + '//! Native [`HostInput`] provider for the process command line, environment, and working directory.\n//!\n//! # When to use\n//! Construct [`OsHostInput`] at a native application edge and provide it to portable code requiring\n//! [`HostInput`]. Tests can replace it with a deterministic provider and keep process state out of\n//! the program under test.\n//!\n//! # Details\n//! The provider owns no persistent state. Each successful lookup copies the host value into\n//! independent [`Bytes`], beginning with a bounded buffer and retrying once at the exact size the\n//! boundary reports. An absent argument or variable remains [`None`]; an unavailable working\n//! directory and contradictory host lengths become [`HostInputError`].\n//!\n//! Constructing the provider reads no host state. Portable code performs lookups after the\n//! application supplies `&mut OsHostInput` for the `&mut HostInput` requirement. Each owned result\n//! also requires an allocator.\n//!\n//! # Gotchas\n//! Reachable OS host-input operations are native-only. Direct WebAssembly compilation rejects them\n//! instead of inventing process-global imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct the native provider without reading process state\n//!\n//! ```silk\n//! import silk.os_host_input as OsHostInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsHostInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n length as bytesLength,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.host_input { HostInput, HostInputError, inputFailure }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`HostInput`] provider for process arguments, environment, and directory.\n///\n/// # Details\n///\n/// The process owns the source values. Each successful byte lookup returns a new owned copy through\n/// the portable service.\npub struct OsHostInput {}\n\n/// Creates a stateless provider for native process input.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut HostInput` to\n/// portable lookup operations in `silk.host_input`.\n///\n/// # Details\n///\n/// Construction performs no lookup and cannot fail. Argument and environment absence remain\n/// ordinary `None` values when a later lookup runs.\npub fn make() -> OsHostInput {\n return OsHostInput {}\n}\n\neffect fn raise() -> never ! HostInputError {\n fail inputFailure()\n}\n\neffect fn rawArgumentCount(count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHostArgumentCount(count, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawArgument(\n index: usize,\n output: &mut [u8],\n count: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe { return run Intrinsic.osHostArgument(index, output, count, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawVariable(\n name: &[u8],\n output: &mut [u8],\n count: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe { return run Intrinsic.osHostVariable(name, output, count, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawWorkingDirectory(\n output: &mut [u8],\n count: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe { return run Intrinsic.osHostWorkingDirectory(output, count, lowReason, nativeCode) }\n return false\n}\n\neffect fn ownedPrefix(view: &[u8], length: usize) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\n/// Copies one host value into independently owned bytes, growing the buffer once when needed.\n///\n/// The low-level boundary reports the value\'s complete byte length even when the buffer it received\n/// was too small, so a second pass with an exactly sized buffer always completes. An absent value\n/// is `None` with the not-found reason; any other reason is a host error.\neffect fn fetch(\n selector: i32,\n index: usize,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let initial = bytesZeroed(128)\n let mut buffer = run initial\n let mut result = none()\n let mut complete = false\n let mut grown = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut total = usize.ZERO\n let mut received = false\n if selector == 0 {\n received = run rawArgument(\n index,\n bytesMutSlice(&mut buffer),\n &mut total,\n &mut lowReason,\n &mut nativeCode\n )\n } else {\n if selector == 1 {\n received = run rawVariable(\n name,\n bytesMutSlice(&mut buffer),\n &mut total,\n &mut lowReason,\n &mut nativeCode\n )\n } else {\n received = run rawWorkingDirectory(\n bytesMutSlice(&mut buffer),\n &mut total,\n &mut lowReason,\n &mut nativeCode\n )\n }\n }\n if received == false {\n if lowReason != 0 { return run raise() }\n complete = true\n } else {\n if total <= bytesLength(&buffer) {\n let owned = run ownedPrefix(bytesSlice(&buffer), total)\n result = some(move owned)\n complete = true\n } else {\n // An exactly sized buffer completes an honest host in one more pass, so a second short\n // answer means the host contradicted the length it just reported.\n if grown { return run raise() }\n grown = true\n let resized = bytesZeroed(total)\n buffer = run resized\n }\n }\n }\n return move result\n}\n\n/// Reports how many arguments the process received, including the program name at index zero.\neffect fn argumentCount(self: &mut OsHostInput) -> usize ! HostInputError {\n let mut count = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let answered = run rawArgumentCount(&mut count, &mut lowReason, &mut nativeCode)\n if answered == false { return run raise() }\n return count\n}\n\n/// Copies one argument\'s raw bytes, exactly as the process received them.\neffect fn argument(\n self: &mut OsHostInput,\n index: usize\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(0, index, stringUtf8Bytes(""))\n}\n\n/// Copies one environment value\'s raw bytes. An unset name is `None` rather than a failure.\neffect fn variable(\n self: &mut OsHostInput,\n name: &[u8]\n) -> Option ! HostInputError | OutOfMemoryError ? &mut Allocator {\n return run fetch(1, usize.ZERO, name)\n}\n\n/// Copies the process working directory\'s raw bytes. It always exists, so absence is a host error.\neffect fn workingDirectory(\n self: &mut OsHostInput\n) -> Bytes ! HostInputError | OutOfMemoryError ? &mut Allocator {\n let found = run fetch(2, usize.ZERO, stringUtf8Bytes(""))\n return match move found {\n Option.Some { value: bytes } => move bytes\n Option.None => run raise()\n }\n}\n\nimpl HostInput for OsHostInput {\n argumentCount: OsHostInput.argumentCount\n argument: OsHostInput.argument\n variable: OsHostInput.variable\n workingDirectory: OsHostInput.workingDirectory\n}\n', }, { module: 'silk/os_monotonic_clock', @@ -916,14 +916,14 @@ export const modules = [ module: 'silk/os_standard_input', path: 'silk/os_standard_input.silk', sourceIdentity: 'silk/os_standard_input', - digest: 'ff1b2df9217443ef2c8286293d4e4712991bf50ba3dfb8270481a48a7d9fef51', + digest: '55abf5b6bab0140de8fcee5f2dae46fcc127b1a761411698118457f7872f7433', documentation: 'silk/os_standard_input.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], runtimeInventory: ['osStandardInputRead'], namespace: 'OsStandardInput', source: - "//! Native [`StandardInput`] provider backed by the process standard-input descriptor.\n//!\n//! # When to use\n//! Construct [`OsStandardInput`] at a native application edge and provide it to portable byte-input\n//! code. Use a scripted provider in tests to control partial reads, end-of-input, and failures.\n//!\n//! # Details\n//! The provider owns no persistent state and commits each host read directly into the caller's\n//! buffer. For a non-empty buffer, a zero-length host transfer becomes the outcome selected by\n//! [`endOfInput`]. Only a host read error becomes [`StreamReadError`]. Partial transfer counts are\n//! preserved exactly.\n//!\n//! Constructing the provider performs no read. Portable code reads after the application supplies\n//! `&mut OsStandardInput` for the `&mut StandardInput` requirement.\n//!\n//! # Gotchas\n//! Reachable OS standard-input operations are native-only. Direct WebAssembly compilation rejects\n//! them instead of inventing a descriptor import. Evaluator execution requires an injected adapter.\n//! The caller must use a non-empty buffer. A zero-capacity host read also transfers zero bytes.\n//!\n//! # Examples\n//! ## Construct the native provider without reading standard input\n//!\n//! ```silk\n//! import silk.os_standard_input as OsStandardInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsStandardInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.i32 as i32\nimport silk.option { Option, none }\nimport silk.standard_input {\n ReadOutcome,\n StandardInput,\n StreamReadError,\n endOfInput,\n filled,\n readFailure\n}\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`StandardInput`] provider for the process input descriptor.\n///\n/// # Details\n///\n/// The process owns the descriptor. Each read changes only the committed prefix of the caller's\n/// buffer and preserves the host transfer count.\npub struct OsStandardInput {}\n\n/// Creates a stateless provider for native standard-input bytes.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut StandardInput` to\n/// portable code that calls `silk.standard_input.receive`.\n///\n/// # Details\n///\n/// Construction performs no read and cannot fail. For a non-empty read buffer, a later zero-byte\n/// host transfer becomes end-of-input data. A host read error becomes [`StreamReadError`].\npub fn make() -> OsStandardInput {\n return OsStandardInput {}\n}\n\neffect fn rawRead(output: &mut [u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osStandardInputRead(output, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn raise() -> never ! StreamReadError {\n fail readFailure()\n}\n\n/// Commits one host read into the caller's buffer.\n///\n/// The low-level boundary reports a zero-length transfer for the end of input, which becomes\n/// `EndOfInput` data rather than a typed failure. Only a host error becomes `StreamReadError`.\n///\n/// The caller must use a non-empty `buffer`. With no capacity, the host also reports a zero-length\n/// transfer and cannot prove permanent end-of-input.\neffect fn read(self: &mut OsStandardInput, buffer: &mut [u8]) -> ReadOutcome ! StreamReadError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let received = run rawRead(move buffer, &mut lowReason, &mut nativeCode)\n let length = match move received {\n Option.None => run raise()\n Option.Some { value: selected } => selected\n }\n if length == usize.ZERO { return endOfInput() }\n return filled(length)\n}\n\nimpl StandardInput for OsStandardInput {\n read: OsStandardInput.read\n}\n", + "//! Native [`StandardInput`] provider backed by the process standard-input descriptor.\n//!\n//! # When to use\n//! Construct [`OsStandardInput`] at a native application edge and provide it to portable byte-input\n//! code. Use a scripted provider in tests to control partial reads, end-of-input, and failures.\n//!\n//! # Details\n//! The provider owns no persistent state and commits each host read directly into the caller's\n//! buffer. For a non-empty buffer, a zero-length host transfer becomes the outcome selected by\n//! [`endOfInput`]. Only a host read error becomes [`StreamReadError`]. Partial transfer counts are\n//! preserved exactly.\n//!\n//! Constructing the provider performs no read. Portable code reads after the application supplies\n//! `&mut OsStandardInput` for the `&mut StandardInput` requirement.\n//!\n//! # Gotchas\n//! Reachable OS standard-input operations are native-only. Direct WebAssembly compilation rejects\n//! them instead of inventing a descriptor import. Evaluator execution requires an injected adapter.\n//! The caller must use a non-empty buffer. A zero-capacity host read also transfers zero bytes.\n//!\n//! # Examples\n//! ## Construct the native provider without reading standard input\n//!\n//! ```silk\n//! import silk.os_standard_input as OsStandardInput\n//!\n//! pub fn main() -> i32 {\n//! let provider = OsStandardInput.make()\n//! drop provider\n//! return 42\n//! }\n//! ```\n\nimport silk.i32 as i32\nimport silk.standard_input {\n ReadOutcome,\n StandardInput,\n StreamReadError,\n endOfInput,\n filled,\n readFailure\n}\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// A stateless native [`StandardInput`] provider for the process input descriptor.\n///\n/// # Details\n///\n/// The process owns the descriptor. Each read changes only the committed prefix of the caller's\n/// buffer and preserves the host transfer count.\npub struct OsStandardInput {}\n\n/// Creates a stateless provider for native standard-input bytes.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut StandardInput` to\n/// portable code that calls `silk.standard_input.receive`.\n///\n/// # Details\n///\n/// Construction performs no read and cannot fail. For a non-empty read buffer, a later zero-byte\n/// host transfer becomes end-of-input data. A host read error becomes [`StreamReadError`].\npub fn make() -> OsStandardInput {\n return OsStandardInput {}\n}\n\neffect fn rawRead(\n output: &mut [u8],\n count: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe { return run Intrinsic.osStandardInputRead(output, count, lowReason, nativeCode) }\n return false\n}\n\neffect fn raise() -> never ! StreamReadError {\n fail readFailure()\n}\n\n/// Commits one host read into the caller's buffer.\n///\n/// The low-level boundary reports a zero-length transfer for the end of input, which becomes\n/// `EndOfInput` data rather than a typed failure. Only a host error becomes `StreamReadError`.\n///\n/// The caller must use a non-empty `buffer`. With no capacity, the host also reports a zero-length\n/// transfer and cannot prove permanent end-of-input.\neffect fn read(self: &mut OsStandardInput, buffer: &mut [u8]) -> ReadOutcome ! StreamReadError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut length = usize.ZERO\n let received = run rawRead(move buffer, &mut length, &mut lowReason, &mut nativeCode)\n if received == false { return run raise() }\n if length == usize.ZERO { return endOfInput() }\n return filled(length)\n}\n\nimpl StandardInput for OsStandardInput {\n read: OsStandardInput.read\n}\n", }, { module: 'silk/os_system_clock', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index b24438007..7f41e7a49 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '9c5a1cb10a043a80930f43ee22f855c6562359b21e1d989e53a3df10154dc907' +export const compilerDigest = '94db0903f9d0f21b1f8fe1a3840da001d3a72fb515beefa1f5abcd2ced70e181' diff --git a/packages/compiler/stdlib/silk/os_child_process.silk b/packages/compiler/stdlib/silk/os_child_process.silk index 9cea94258..d707cf410 100644 --- a/packages/compiler/stdlib/silk/os_child_process.silk +++ b/packages/compiler/stdlib/silk/os_child_process.silk @@ -67,7 +67,6 @@ import silk.child_process { import silk.allocator { Allocator } import silk.allocator { OutOfMemoryError } import silk.i32 as i32 -import silk.option { Option, none } import silk.u32 as u32 import silk.u8 as u8 import silk.usize as usize @@ -146,14 +145,14 @@ effect fn rawCapture( stream: i32, offset: usize, output: &mut [u8], + count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32 -) -> Option { +) -> bool { unsafe { - return run Intrinsic.osProcessCapture(stream, offset, output, lowReason, nativeCode) + return run Intrinsic.osProcessCapture(stream, offset, output, count, lowReason, nativeCode) } - let impossible = 1 / 0 - return none() + return false } /// Copies one completed capture out of the boundary into independently owned bytes. @@ -170,11 +169,16 @@ effect fn drain(stream: i32, length: usize) -> Bytes ! ProcessError | OutOfMemor let mut lowReason = 0 let mut nativeCode = u32.toU32(0) let mut output = bytesMutSlice(&mut buffer) - let transferred = run rawCapture(stream, offset, move output, &mut lowReason, &mut nativeCode) - let received = match move transferred { - Option.None => run raise(captureOperation(), lowReason, nativeCode) - Option.Some { value: selected } => selected - } + let mut received = usize.ZERO + let transferred = run rawCapture( + stream, + offset, + move output, + &mut received, + &mut lowReason, + &mut nativeCode + ) + if transferred == false { return run raise(captureOperation(), lowReason, nativeCode) } if received == usize.ZERO { return run raise(captureOperation(), 10, u32.toU32(0)) } let view = bytesSlice(&buffer) let mut index = usize.ZERO diff --git a/packages/compiler/stdlib/silk/os_filesystem.silk b/packages/compiler/stdlib/silk/os_filesystem.silk index 851412ee8..0dc8eaa17 100644 --- a/packages/compiler/stdlib/silk/os_filesystem.silk +++ b/packages/compiler/stdlib/silk/os_filesystem.silk @@ -283,14 +283,18 @@ effect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryErro let mut lowReason = 0 let mut nativeCode = u32.toU32(0) let mut output = bytesMutSlice(&mut buffer) - let mut received = none() + let mut length = usize.ZERO + let mut received = false unsafe { - received = run Intrinsic.osFileRead(handle, output, &mut lowReason, &mut nativeCode) - } - let length = match move received { - Option.None => run raise(readFileOperation(), lowReason, nativeCode) - Option.Some { value: selected } => selected + received = run Intrinsic.osFileRead( + handle, + output, + &mut length, + &mut lowReason, + &mut nativeCode + ) } + if received == false { return run raise(readFileOperation(), lowReason, nativeCode) } if length == usize.ZERO { complete = true } else { @@ -331,14 +335,19 @@ effect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError { let mut lowReason = 0 let mut nativeCode = u32.toU32(0) // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy. - let mut written = none() + let mut length = usize.ZERO + let mut written = false unsafe { - written = run Intrinsic.osFileWrite(handle, bytes, offset, &mut lowReason, &mut nativeCode) - } - let length = match move written { - Option.None => run raise(writeFileOperation(), lowReason, nativeCode) - Option.Some { value: selected } => selected + written = run Intrinsic.osFileWrite( + handle, + bytes, + offset, + &mut length, + &mut lowReason, + &mut nativeCode + ) } + if written == false { return run raise(writeFileOperation(), lowReason, nativeCode) } if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) } offset = offset + length } @@ -383,15 +392,20 @@ effect fn listLoop( let mut lowReason = 0 let mut nativeCode = u32.toU32(0) let mut output = bytesMutSlice(&mut buffer) - let mut next = none() + let mut length = usize.ZERO + let mut next = false unsafe { - next = run Intrinsic.osDirectoryNext(handle, output, &mut kind, &mut required, &mut lowReason, &mut nativeCode) - } - let encodedLength = match move next { - Option.Some { value: presentLength } => presentLength + usize.ONE - Option.None => usize.ZERO + next = run Intrinsic.osDirectoryNext( + handle, + output, + &mut length, + &mut kind, + &mut required, + &mut lowReason, + &mut nativeCode + ) } - if encodedLength == usize.ZERO { + if next == false { if lowReason == 8 { let resized = bytesZeroed(required) buffer = run resized @@ -399,7 +413,6 @@ effect fn listLoop( return run raise(listDirectoryOperation(), lowReason, nativeCode) } } else { - let length = encodedLength - usize.ONE if length == usize.ZERO { complete = true } else { @@ -498,15 +511,24 @@ effect fn rawCreateUnique( parent: &[u8], prefix: &[u8], output: &mut [u8], + count: &mut usize, required: &mut usize, lowReason: &mut i32, nativeCode: &mut u32 -) -> Option { +) -> bool { unsafe { - return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, output, required, lowReason, nativeCode) + return run Intrinsic.osDirectoryCreateUnique( + root, + parent, + prefix, + output, + count, + required, + lowReason, + nativeCode + ) } - let impossible = 1 / 0 - return none() + return false } /// Creates one uniquely named directory under `parent` and returns its complete Path. @@ -527,20 +549,18 @@ effect fn createTemporaryDirectory( let mut lowReason = 0 let mut nativeCode = u32.toU32(0) let mut output = bytesMutSlice(&mut buffer) + let mut length = usize.ZERO let chosen = run rawCreateUnique( bytesSlice(&self.root), pathBytes(parent), prefix, move output, + &mut length, &mut required, &mut lowReason, &mut nativeCode ) - let length = match move chosen { - Option.Some { value: selected } => selected - Option.None => usize.ZERO - } - if length == usize.ZERO { + if chosen == false { if lowReason == 8 { let resized = bytesZeroed(required) buffer = run resized diff --git a/packages/compiler/stdlib/silk/os_host_input.silk b/packages/compiler/stdlib/silk/os_host_input.silk index 9d6c93d7c..af74d4d0a 100644 --- a/packages/compiler/stdlib/silk/os_host_input.silk +++ b/packages/compiler/stdlib/silk/os_host_input.silk @@ -87,33 +87,33 @@ effect fn rawArgumentCount(count: &mut usize, lowReason: &mut i32, nativeCode: & effect fn rawArgument( index: usize, output: &mut [u8], + count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32 -) -> Option { - unsafe { return run Intrinsic.osHostArgument(index, output, lowReason, nativeCode) } - let impossible = 1 / 0 - return none() +) -> bool { + unsafe { return run Intrinsic.osHostArgument(index, output, count, lowReason, nativeCode) } + return false } effect fn rawVariable( name: &[u8], output: &mut [u8], + count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32 -) -> Option { - unsafe { return run Intrinsic.osHostVariable(name, output, lowReason, nativeCode) } - let impossible = 1 / 0 - return none() +) -> bool { + unsafe { return run Intrinsic.osHostVariable(name, output, count, lowReason, nativeCode) } + return false } effect fn rawWorkingDirectory( output: &mut [u8], + count: &mut usize, lowReason: &mut i32, nativeCode: &mut u32 -) -> Option { - unsafe { return run Intrinsic.osHostWorkingDirectory(output, lowReason, nativeCode) } - let impossible = 1 / 0 - return none() +) -> bool { + unsafe { return run Intrinsic.osHostWorkingDirectory(output, count, lowReason, nativeCode) } + return false } effect fn ownedPrefix(view: &[u8], length: usize) -> Bytes ! OutOfMemoryError ? &mut Allocator { @@ -145,25 +145,38 @@ effect fn fetch( while complete == false { let mut lowReason = 0 let mut nativeCode = u32.toU32(0) - let mut received = none() + let mut total = usize.ZERO + let mut received = false if selector == 0 { - received = run rawArgument(index, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode) + received = run rawArgument( + index, + bytesMutSlice(&mut buffer), + &mut total, + &mut lowReason, + &mut nativeCode + ) } else { if selector == 1 { - received = run rawVariable(name, bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode) + received = run rawVariable( + name, + bytesMutSlice(&mut buffer), + &mut total, + &mut lowReason, + &mut nativeCode + ) } else { - received = run rawWorkingDirectory(bytesMutSlice(&mut buffer), &mut lowReason, &mut nativeCode) + received = run rawWorkingDirectory( + bytesMutSlice(&mut buffer), + &mut total, + &mut lowReason, + &mut nativeCode + ) } } - let encoded = match move received { - Option.Some { value: total } => total + usize.ONE - Option.None => usize.ZERO - } - if encoded == usize.ZERO { + if received == false { if lowReason != 0 { return run raise() } complete = true } else { - let total = encoded - usize.ONE if total <= bytesLength(&buffer) { let owned = run ownedPrefix(bytesSlice(&buffer), total) result = some(move owned) diff --git a/packages/compiler/stdlib/silk/os_standard_input.silk b/packages/compiler/stdlib/silk/os_standard_input.silk index b8167cbce..bb5e503bd 100644 --- a/packages/compiler/stdlib/silk/os_standard_input.silk +++ b/packages/compiler/stdlib/silk/os_standard_input.silk @@ -32,7 +32,6 @@ //! ``` import silk.i32 as i32 -import silk.option { Option, none } import silk.standard_input { ReadOutcome, StandardInput, @@ -68,10 +67,14 @@ pub fn make() -> OsStandardInput { return OsStandardInput {} } -effect fn rawRead(output: &mut [u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option { - unsafe { return run Intrinsic.osStandardInputRead(output, lowReason, nativeCode) } - let impossible = 1 / 0 - return none() +effect fn rawRead( + output: &mut [u8], + count: &mut usize, + lowReason: &mut i32, + nativeCode: &mut u32 +) -> bool { + unsafe { return run Intrinsic.osStandardInputRead(output, count, lowReason, nativeCode) } + return false } effect fn raise() -> never ! StreamReadError { @@ -88,11 +91,9 @@ effect fn raise() -> never ! StreamReadError { effect fn read(self: &mut OsStandardInput, buffer: &mut [u8]) -> ReadOutcome ! StreamReadError { let mut lowReason = 0 let mut nativeCode = u32.toU32(0) - let received = run rawRead(move buffer, &mut lowReason, &mut nativeCode) - let length = match move received { - Option.None => run raise() - Option.Some { value: selected } => selected - } + let mut length = usize.ZERO + let received = run rawRead(move buffer, &mut length, &mut lowReason, &mut nativeCode) + if received == false { return run raise() } if length == usize.ZERO { return endOfInput() } return filled(length) } diff --git a/packages/compiler/test/ChildProcess.test.ts b/packages/compiler/test/ChildProcess.test.ts index f958027b4..02a3c2a79 100644 --- a/packages/compiler/test/ChildProcess.test.ts +++ b/packages/compiler/test/ChildProcess.test.ts @@ -114,7 +114,7 @@ import silk.filesystem { FileError } import silk.option { Option } import silk.result { Result } pub fn main() -> i32 { - let attempted = run Intrinsic.effectResult(program()) + let attempted = run Effect.result(program()) return match move attempted { Result.Success { value } => value Result.Failure { error } => match move error { diff --git a/packages/compiler/test/FileSystemAcceptance.test.ts b/packages/compiler/test/FileSystemAcceptance.test.ts index eb5041622..41089f88f 100644 --- a/packages/compiler/test/FileSystemAcceptance.test.ts +++ b/packages/compiler/test/FileSystemAcceptance.test.ts @@ -238,7 +238,7 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { } pub fn main() -> i32 { - let completed = run Intrinsic.effectResult(program()) + let completed = run Effect.result(program()) return match move completed { Result.Success { value } => value Result.Failure { error } => 10 @@ -280,7 +280,7 @@ effect fn check() -> i32 ! FileError | OutOfMemoryError { } pub fn main() -> i32 { - let completed = run Intrinsic.effectResult(check()) + let completed = run Effect.result(check()) return match move completed { Result.Success { value } => value Result.Failure { error } => 2 diff --git a/packages/compiler/test/IntrinsicCatalog.test.ts b/packages/compiler/test/IntrinsicCatalog.test.ts index 9740df852..1556858b1 100644 --- a/packages/compiler/test/IntrinsicCatalog.test.ts +++ b/packages/compiler/test/IntrinsicCatalog.test.ts @@ -272,21 +272,21 @@ effect fn fileOpen(root: &[u8], path: &[u8], reason: &mut i32, code: &mut u32) - unsafe { return run Intrinsic.osFileOpen(root, path, 0, reason, code) } return none() } -effect fn fileRead(handle: &mut OsHandle, output: &mut [u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osFileRead(handle, output, reason, code) } - return none() +effect fn fileRead(handle: &mut OsHandle, output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osFileRead(handle, output, count, reason, code) } + return false } -effect fn fileWrite(handle: &mut OsHandle, input: &[u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osFileWrite(handle, input, 0, reason, code) } - return none() +effect fn fileWrite(handle: &mut OsHandle, input: &[u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osFileWrite(handle, input, 0, count, reason, code) } + return false } effect fn directoryOpen(root: &[u8], path: &[u8], reason: &mut i32, code: &mut u32) -> Option { unsafe { return run Intrinsic.osDirectoryOpen(root, path, reason, code) } return none() } -effect fn directoryNext(handle: &mut OsHandle, output: &mut [u8], kind: &mut i32, required: &mut usize, reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osDirectoryNext(handle, output, kind, required, reason, code) } - return none() +effect fn directoryNext(handle: &mut OsHandle, output: &mut [u8], count: &mut usize, kind: &mut i32, required: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osDirectoryNext(handle, output, count, kind, required, reason, code) } + return false } effect fn inspect(root: &[u8], path: &[u8], kind: &mut i32, length: &mut usize, reason: &mut i32, code: &mut u32) -> bool { unsafe { return run Intrinsic.osPathInspect(root, path, kind, length, reason, code) } @@ -296,9 +296,9 @@ effect fn create(root: &[u8], path: &[u8], reason: &mut i32, code: &mut u32) -> unsafe { return run Intrinsic.osDirectoryCreate(root, path, reason, code) } return false } -effect fn createUnique(root: &[u8], parent: &[u8], prefix: &[u8], output: &mut [u8], required: &mut usize, reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, move output, required, reason, code) } - return none() +effect fn createUnique(root: &[u8], parent: &[u8], prefix: &[u8], output: &mut [u8], count: &mut usize, required: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osDirectoryCreateUnique(root, parent, prefix, move output, count, required, reason, code) } + return false } effect fn removeFile(root: &[u8], path: &[u8], reason: &mut i32, code: &mut u32) -> bool { unsafe { return run Intrinsic.osFileRemove(root, path, reason, code) } @@ -312,33 +312,33 @@ effect fn close(handle: OsHandle, reason: &mut i32, code: &mut u32) -> bool { unsafe { return run Intrinsic.osHandleClose(move handle, reason, code) } return false } -effect fn standardInputRead(output: &mut [u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osStandardInputRead(move output, reason, code) } - return none() +effect fn standardInputRead(output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osStandardInputRead(move output, count, reason, code) } + return false } effect fn processExecute(program: &[u8], arguments: &[u8], environment: &[u8], directory: &[u8], status: &mut i32, exit: &mut i32, outputLength: &mut usize, errorLength: &mut usize, reason: &mut i32, code: &mut u32) -> bool { unsafe { return run Intrinsic.osProcessExecute(program, arguments, environment, directory, status, exit, outputLength, errorLength, reason, code) } return false } -effect fn processCapture(output: &mut [u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osProcessCapture(0, usize.ZERO, move output, reason, code) } - return none() +effect fn processCapture(output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osProcessCapture(0, usize.ZERO, move output, count, reason, code) } + return false } effect fn hostArgumentCount(count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { unsafe { return run Intrinsic.osHostArgumentCount(count, reason, code) } return false } -effect fn hostArgument(index: usize, output: &mut [u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osHostArgument(index, move output, reason, code) } - return none() +effect fn hostArgument(index: usize, output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osHostArgument(index, move output, count, reason, code) } + return false } -effect fn hostVariable(name: &[u8], output: &mut [u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osHostVariable(name, move output, reason, code) } - return none() +effect fn hostVariable(name: &[u8], output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osHostVariable(name, move output, count, reason, code) } + return false } -effect fn hostWorkingDirectory(output: &mut [u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osHostWorkingDirectory(move output, reason, code) } - return none() +effect fn hostWorkingDirectory(output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { + unsafe { return run Intrinsic.osHostWorkingDirectory(move output, count, reason, code) } + return false }`, ]) diff --git a/packages/compiler/test/OsFileSystem.test.ts b/packages/compiler/test/OsFileSystem.test.ts index 93a90563e..f3ef246e3 100644 --- a/packages/compiler/test/OsFileSystem.test.ts +++ b/packages/compiler/test/OsFileSystem.test.ts @@ -193,7 +193,7 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { } pub fn main() -> i32 { - let attempted = run Intrinsic.effectResult(program()) + let attempted = run Effect.result(program()) return match move attempted { Result.Success { value } => value Result.Failure { error } => match move error { @@ -311,7 +311,7 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { } pub fn main() -> i32 { - let attempted = run Intrinsic.effectResult(program()) + let attempted = run Effect.result(program()) return match move attempted { Result.Success { value } => value Result.Failure { error } => 10 @@ -659,7 +659,7 @@ effect fn program() -> i32 ! FileError | OutOfMemoryError { } pub fn main() -> i32 { - let completed = run Intrinsic.effectResult(program()) + let completed = run Effect.result(program()) return match move completed { Result.Success { value } => value Result.Failure { error } => 10 diff --git a/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts b/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts index 64ee37966..9c9e88337 100644 --- a/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts +++ b/packages/compiler/test/TemporaryDirectoryAcceptance.test.ts @@ -30,6 +30,7 @@ afterAll(() => { * being handed to `Effect.ensuring`, whose finalizer is typed `! never`. */ const prelude = `import silk.os_filesystem { make as osMake } +import silk.effect as Effect import silk.filesystem { FileError, FileSystem, Path, TemporaryDirectory, createDirectoriesRecursively, exists, fromBytes as pathFromBytes, join as pathJoin, @@ -50,7 +51,7 @@ const epilogue = `import silk.allocator { OutOfMemoryError } import silk.filesystem { FileError } import silk.result { Result } pub fn main() -> i32 { - let completed = run Intrinsic.effectResult(program()) + let completed = run Effect.result(program()) return match move completed { Result.Success { value } => value Result.Failure { error } => match move error { diff --git a/packages/compiler/test/fixtures/intrinsic-inventory.json b/packages/compiler/test/fixtures/intrinsic-inventory.json index d4478bdbc..ae33d5ffd 100644 --- a/packages/compiler/test/fixtures/intrinsic-inventory.json +++ b/packages/compiler/test/fixtures/intrinsic-inventory.json @@ -1,5 +1,9 @@ { - "targets": ["Evaluator", "LLVM", "Wasm"], + "targets": [ + "Evaluator", + "LLVM", + "Wasm" + ], "entries": [ { "operation": "Intrinsic.boolEquals", @@ -35,7 +39,7 @@ }, { "operation": "Intrinsic.u8CheckedToU8", - "signature": "fn Intrinsic.u8CheckedToU8(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToU8(value: u8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToU8", @@ -51,7 +55,7 @@ }, { "operation": "Intrinsic.u8CheckedToU16", - "signature": "fn Intrinsic.u8CheckedToU16(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToU16(value: u8, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToU16", @@ -67,7 +71,7 @@ }, { "operation": "Intrinsic.u8CheckedToU32", - "signature": "fn Intrinsic.u8CheckedToU32(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToU32(value: u8, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToU32", @@ -83,7 +87,7 @@ }, { "operation": "Intrinsic.u8CheckedToU64", - "signature": "fn Intrinsic.u8CheckedToU64(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToU64(value: u8, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToU64", @@ -99,7 +103,7 @@ }, { "operation": "Intrinsic.u8CheckedToUsize", - "signature": "fn Intrinsic.u8CheckedToUsize(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToUsize(value: u8, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToUsize", @@ -115,7 +119,7 @@ }, { "operation": "Intrinsic.u8CheckedToI8", - "signature": "fn Intrinsic.u8CheckedToI8(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToI8(value: u8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToI8", @@ -131,7 +135,7 @@ }, { "operation": "Intrinsic.u8CheckedToI16", - "signature": "fn Intrinsic.u8CheckedToI16(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToI16(value: u8, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToI16", @@ -147,7 +151,7 @@ }, { "operation": "Intrinsic.u8CheckedToI32", - "signature": "fn Intrinsic.u8CheckedToI32(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToI32(value: u8, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToI32", @@ -163,7 +167,7 @@ }, { "operation": "Intrinsic.u8CheckedToI64", - "signature": "fn Intrinsic.u8CheckedToI64(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToI64(value: u8, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToI64", @@ -179,7 +183,7 @@ }, { "operation": "Intrinsic.u8CheckedToIsize", - "signature": "fn Intrinsic.u8CheckedToIsize(value: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedToIsize(value: u8, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedToIsize", @@ -355,7 +359,7 @@ }, { "operation": "Intrinsic.u8CheckedAdd", - "signature": "fn Intrinsic.u8CheckedAdd(left: u8, right: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedAdd(left: u8, right: u8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedAdd", @@ -363,7 +367,7 @@ }, { "operation": "Intrinsic.u8CheckedSubtract", - "signature": "fn Intrinsic.u8CheckedSubtract(left: u8, right: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedSubtract(left: u8, right: u8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedSubtract", @@ -371,7 +375,7 @@ }, { "operation": "Intrinsic.u8CheckedMultiply", - "signature": "fn Intrinsic.u8CheckedMultiply(left: u8, right: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedMultiply(left: u8, right: u8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedMultiply", @@ -379,7 +383,7 @@ }, { "operation": "Intrinsic.u8CheckedDivide", - "signature": "fn Intrinsic.u8CheckedDivide(left: u8, right: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedDivide(left: u8, right: u8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedDivide", @@ -387,7 +391,7 @@ }, { "operation": "Intrinsic.u8CheckedRemainder", - "signature": "fn Intrinsic.u8CheckedRemainder(left: u8, right: u8) -> Option", + "signature": "fn Intrinsic.u8CheckedRemainder(left: u8, right: u8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u8.checkedRemainder", @@ -451,7 +455,7 @@ }, { "operation": "Intrinsic.u16CheckedToU8", - "signature": "fn Intrinsic.u16CheckedToU8(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToU8(value: u16, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToU8", @@ -467,7 +471,7 @@ }, { "operation": "Intrinsic.u16CheckedToU16", - "signature": "fn Intrinsic.u16CheckedToU16(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToU16(value: u16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToU16", @@ -483,7 +487,7 @@ }, { "operation": "Intrinsic.u16CheckedToU32", - "signature": "fn Intrinsic.u16CheckedToU32(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToU32(value: u16, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToU32", @@ -499,7 +503,7 @@ }, { "operation": "Intrinsic.u16CheckedToU64", - "signature": "fn Intrinsic.u16CheckedToU64(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToU64(value: u16, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToU64", @@ -515,7 +519,7 @@ }, { "operation": "Intrinsic.u16CheckedToUsize", - "signature": "fn Intrinsic.u16CheckedToUsize(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToUsize(value: u16, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToUsize", @@ -531,7 +535,7 @@ }, { "operation": "Intrinsic.u16CheckedToI8", - "signature": "fn Intrinsic.u16CheckedToI8(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToI8(value: u16, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToI8", @@ -547,7 +551,7 @@ }, { "operation": "Intrinsic.u16CheckedToI16", - "signature": "fn Intrinsic.u16CheckedToI16(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToI16(value: u16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToI16", @@ -563,7 +567,7 @@ }, { "operation": "Intrinsic.u16CheckedToI32", - "signature": "fn Intrinsic.u16CheckedToI32(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToI32(value: u16, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToI32", @@ -579,7 +583,7 @@ }, { "operation": "Intrinsic.u16CheckedToI64", - "signature": "fn Intrinsic.u16CheckedToI64(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToI64(value: u16, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToI64", @@ -595,7 +599,7 @@ }, { "operation": "Intrinsic.u16CheckedToIsize", - "signature": "fn Intrinsic.u16CheckedToIsize(value: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedToIsize(value: u16, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedToIsize", @@ -771,7 +775,7 @@ }, { "operation": "Intrinsic.u16CheckedAdd", - "signature": "fn Intrinsic.u16CheckedAdd(left: u16, right: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedAdd(left: u16, right: u16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedAdd", @@ -779,7 +783,7 @@ }, { "operation": "Intrinsic.u16CheckedSubtract", - "signature": "fn Intrinsic.u16CheckedSubtract(left: u16, right: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedSubtract(left: u16, right: u16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedSubtract", @@ -787,7 +791,7 @@ }, { "operation": "Intrinsic.u16CheckedMultiply", - "signature": "fn Intrinsic.u16CheckedMultiply(left: u16, right: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedMultiply(left: u16, right: u16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedMultiply", @@ -795,7 +799,7 @@ }, { "operation": "Intrinsic.u16CheckedDivide", - "signature": "fn Intrinsic.u16CheckedDivide(left: u16, right: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedDivide(left: u16, right: u16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedDivide", @@ -803,7 +807,7 @@ }, { "operation": "Intrinsic.u16CheckedRemainder", - "signature": "fn Intrinsic.u16CheckedRemainder(left: u16, right: u16) -> Option", + "signature": "fn Intrinsic.u16CheckedRemainder(left: u16, right: u16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u16.checkedRemainder", @@ -867,7 +871,7 @@ }, { "operation": "Intrinsic.u32CheckedToU8", - "signature": "fn Intrinsic.u32CheckedToU8(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToU8(value: u32, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToU8", @@ -883,7 +887,7 @@ }, { "operation": "Intrinsic.u32CheckedToU16", - "signature": "fn Intrinsic.u32CheckedToU16(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToU16(value: u32, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToU16", @@ -899,7 +903,7 @@ }, { "operation": "Intrinsic.u32CheckedToU32", - "signature": "fn Intrinsic.u32CheckedToU32(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToU32(value: u32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToU32", @@ -915,7 +919,7 @@ }, { "operation": "Intrinsic.u32CheckedToU64", - "signature": "fn Intrinsic.u32CheckedToU64(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToU64(value: u32, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToU64", @@ -931,7 +935,7 @@ }, { "operation": "Intrinsic.u32CheckedToUsize", - "signature": "fn Intrinsic.u32CheckedToUsize(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToUsize(value: u32, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToUsize", @@ -947,7 +951,7 @@ }, { "operation": "Intrinsic.u32CheckedToI8", - "signature": "fn Intrinsic.u32CheckedToI8(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToI8(value: u32, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToI8", @@ -963,7 +967,7 @@ }, { "operation": "Intrinsic.u32CheckedToI16", - "signature": "fn Intrinsic.u32CheckedToI16(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToI16(value: u32, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToI16", @@ -979,7 +983,7 @@ }, { "operation": "Intrinsic.u32CheckedToI32", - "signature": "fn Intrinsic.u32CheckedToI32(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToI32(value: u32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToI32", @@ -995,7 +999,7 @@ }, { "operation": "Intrinsic.u32CheckedToI64", - "signature": "fn Intrinsic.u32CheckedToI64(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToI64(value: u32, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToI64", @@ -1011,7 +1015,7 @@ }, { "operation": "Intrinsic.u32CheckedToIsize", - "signature": "fn Intrinsic.u32CheckedToIsize(value: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedToIsize(value: u32, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedToIsize", @@ -1187,7 +1191,7 @@ }, { "operation": "Intrinsic.u32CheckedAdd", - "signature": "fn Intrinsic.u32CheckedAdd(left: u32, right: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedAdd(left: u32, right: u32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedAdd", @@ -1195,7 +1199,7 @@ }, { "operation": "Intrinsic.u32CheckedSubtract", - "signature": "fn Intrinsic.u32CheckedSubtract(left: u32, right: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedSubtract(left: u32, right: u32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedSubtract", @@ -1203,7 +1207,7 @@ }, { "operation": "Intrinsic.u32CheckedMultiply", - "signature": "fn Intrinsic.u32CheckedMultiply(left: u32, right: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedMultiply(left: u32, right: u32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedMultiply", @@ -1211,7 +1215,7 @@ }, { "operation": "Intrinsic.u32CheckedDivide", - "signature": "fn Intrinsic.u32CheckedDivide(left: u32, right: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedDivide(left: u32, right: u32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedDivide", @@ -1219,7 +1223,7 @@ }, { "operation": "Intrinsic.u32CheckedRemainder", - "signature": "fn Intrinsic.u32CheckedRemainder(left: u32, right: u32) -> Option", + "signature": "fn Intrinsic.u32CheckedRemainder(left: u32, right: u32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u32.checkedRemainder", @@ -1283,7 +1287,7 @@ }, { "operation": "Intrinsic.u64CheckedToU8", - "signature": "fn Intrinsic.u64CheckedToU8(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToU8(value: u64, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToU8", @@ -1299,7 +1303,7 @@ }, { "operation": "Intrinsic.u64CheckedToU16", - "signature": "fn Intrinsic.u64CheckedToU16(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToU16(value: u64, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToU16", @@ -1315,7 +1319,7 @@ }, { "operation": "Intrinsic.u64CheckedToU32", - "signature": "fn Intrinsic.u64CheckedToU32(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToU32(value: u64, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToU32", @@ -1331,7 +1335,7 @@ }, { "operation": "Intrinsic.u64CheckedToU64", - "signature": "fn Intrinsic.u64CheckedToU64(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToU64(value: u64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToU64", @@ -1347,7 +1351,7 @@ }, { "operation": "Intrinsic.u64CheckedToUsize", - "signature": "fn Intrinsic.u64CheckedToUsize(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToUsize(value: u64, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToUsize", @@ -1363,7 +1367,7 @@ }, { "operation": "Intrinsic.u64CheckedToI8", - "signature": "fn Intrinsic.u64CheckedToI8(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToI8(value: u64, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToI8", @@ -1379,7 +1383,7 @@ }, { "operation": "Intrinsic.u64CheckedToI16", - "signature": "fn Intrinsic.u64CheckedToI16(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToI16(value: u64, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToI16", @@ -1395,7 +1399,7 @@ }, { "operation": "Intrinsic.u64CheckedToI32", - "signature": "fn Intrinsic.u64CheckedToI32(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToI32(value: u64, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToI32", @@ -1411,7 +1415,7 @@ }, { "operation": "Intrinsic.u64CheckedToI64", - "signature": "fn Intrinsic.u64CheckedToI64(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToI64(value: u64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToI64", @@ -1427,7 +1431,7 @@ }, { "operation": "Intrinsic.u64CheckedToIsize", - "signature": "fn Intrinsic.u64CheckedToIsize(value: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedToIsize(value: u64, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedToIsize", @@ -1603,7 +1607,7 @@ }, { "operation": "Intrinsic.u64CheckedAdd", - "signature": "fn Intrinsic.u64CheckedAdd(left: u64, right: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedAdd(left: u64, right: u64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedAdd", @@ -1611,7 +1615,7 @@ }, { "operation": "Intrinsic.u64CheckedSubtract", - "signature": "fn Intrinsic.u64CheckedSubtract(left: u64, right: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedSubtract(left: u64, right: u64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedSubtract", @@ -1619,7 +1623,7 @@ }, { "operation": "Intrinsic.u64CheckedMultiply", - "signature": "fn Intrinsic.u64CheckedMultiply(left: u64, right: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedMultiply(left: u64, right: u64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedMultiply", @@ -1627,7 +1631,7 @@ }, { "operation": "Intrinsic.u64CheckedDivide", - "signature": "fn Intrinsic.u64CheckedDivide(left: u64, right: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedDivide(left: u64, right: u64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedDivide", @@ -1635,7 +1639,7 @@ }, { "operation": "Intrinsic.u64CheckedRemainder", - "signature": "fn Intrinsic.u64CheckedRemainder(left: u64, right: u64) -> Option", + "signature": "fn Intrinsic.u64CheckedRemainder(left: u64, right: u64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/u64.checkedRemainder", @@ -1699,7 +1703,7 @@ }, { "operation": "Intrinsic.usizeCheckedToU8", - "signature": "fn Intrinsic.usizeCheckedToU8(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToU8(value: usize, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToU8", @@ -1715,7 +1719,7 @@ }, { "operation": "Intrinsic.usizeCheckedToU16", - "signature": "fn Intrinsic.usizeCheckedToU16(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToU16(value: usize, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToU16", @@ -1731,7 +1735,7 @@ }, { "operation": "Intrinsic.usizeCheckedToU32", - "signature": "fn Intrinsic.usizeCheckedToU32(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToU32(value: usize, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToU32", @@ -1747,7 +1751,7 @@ }, { "operation": "Intrinsic.usizeCheckedToU64", - "signature": "fn Intrinsic.usizeCheckedToU64(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToU64(value: usize, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToU64", @@ -1763,7 +1767,7 @@ }, { "operation": "Intrinsic.usizeCheckedToUsize", - "signature": "fn Intrinsic.usizeCheckedToUsize(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToUsize(value: usize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToUsize", @@ -1779,7 +1783,7 @@ }, { "operation": "Intrinsic.usizeCheckedToI8", - "signature": "fn Intrinsic.usizeCheckedToI8(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToI8(value: usize, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToI8", @@ -1795,7 +1799,7 @@ }, { "operation": "Intrinsic.usizeCheckedToI16", - "signature": "fn Intrinsic.usizeCheckedToI16(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToI16(value: usize, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToI16", @@ -1811,7 +1815,7 @@ }, { "operation": "Intrinsic.usizeCheckedToI32", - "signature": "fn Intrinsic.usizeCheckedToI32(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToI32(value: usize, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToI32", @@ -1827,7 +1831,7 @@ }, { "operation": "Intrinsic.usizeCheckedToI64", - "signature": "fn Intrinsic.usizeCheckedToI64(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToI64(value: usize, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToI64", @@ -1843,7 +1847,7 @@ }, { "operation": "Intrinsic.usizeCheckedToIsize", - "signature": "fn Intrinsic.usizeCheckedToIsize(value: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedToIsize(value: usize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedToIsize", @@ -2019,7 +2023,7 @@ }, { "operation": "Intrinsic.usizeCheckedAdd", - "signature": "fn Intrinsic.usizeCheckedAdd(left: usize, right: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedAdd(left: usize, right: usize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedAdd", @@ -2027,7 +2031,7 @@ }, { "operation": "Intrinsic.usizeCheckedSubtract", - "signature": "fn Intrinsic.usizeCheckedSubtract(left: usize, right: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedSubtract(left: usize, right: usize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedSubtract", @@ -2035,7 +2039,7 @@ }, { "operation": "Intrinsic.usizeCheckedMultiply", - "signature": "fn Intrinsic.usizeCheckedMultiply(left: usize, right: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedMultiply(left: usize, right: usize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedMultiply", @@ -2043,7 +2047,7 @@ }, { "operation": "Intrinsic.usizeCheckedDivide", - "signature": "fn Intrinsic.usizeCheckedDivide(left: usize, right: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedDivide(left: usize, right: usize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedDivide", @@ -2051,7 +2055,7 @@ }, { "operation": "Intrinsic.usizeCheckedRemainder", - "signature": "fn Intrinsic.usizeCheckedRemainder(left: usize, right: usize) -> Option", + "signature": "fn Intrinsic.usizeCheckedRemainder(left: usize, right: usize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/usize.checkedRemainder", @@ -2139,7 +2143,7 @@ }, { "operation": "Intrinsic.i8CheckedToU8", - "signature": "fn Intrinsic.i8CheckedToU8(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToU8(value: i8, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToU8", @@ -2155,7 +2159,7 @@ }, { "operation": "Intrinsic.i8CheckedToU16", - "signature": "fn Intrinsic.i8CheckedToU16(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToU16(value: i8, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToU16", @@ -2171,7 +2175,7 @@ }, { "operation": "Intrinsic.i8CheckedToU32", - "signature": "fn Intrinsic.i8CheckedToU32(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToU32(value: i8, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToU32", @@ -2187,7 +2191,7 @@ }, { "operation": "Intrinsic.i8CheckedToU64", - "signature": "fn Intrinsic.i8CheckedToU64(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToU64(value: i8, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToU64", @@ -2203,7 +2207,7 @@ }, { "operation": "Intrinsic.i8CheckedToUsize", - "signature": "fn Intrinsic.i8CheckedToUsize(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToUsize(value: i8, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToUsize", @@ -2219,7 +2223,7 @@ }, { "operation": "Intrinsic.i8CheckedToI8", - "signature": "fn Intrinsic.i8CheckedToI8(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToI8(value: i8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToI8", @@ -2235,7 +2239,7 @@ }, { "operation": "Intrinsic.i8CheckedToI16", - "signature": "fn Intrinsic.i8CheckedToI16(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToI16(value: i8, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToI16", @@ -2251,7 +2255,7 @@ }, { "operation": "Intrinsic.i8CheckedToI32", - "signature": "fn Intrinsic.i8CheckedToI32(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToI32(value: i8, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToI32", @@ -2267,7 +2271,7 @@ }, { "operation": "Intrinsic.i8CheckedToI64", - "signature": "fn Intrinsic.i8CheckedToI64(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToI64(value: i8, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToI64", @@ -2283,7 +2287,7 @@ }, { "operation": "Intrinsic.i8CheckedToIsize", - "signature": "fn Intrinsic.i8CheckedToIsize(value: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedToIsize(value: i8, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedToIsize", @@ -2459,7 +2463,7 @@ }, { "operation": "Intrinsic.i8CheckedAdd", - "signature": "fn Intrinsic.i8CheckedAdd(left: i8, right: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedAdd(left: i8, right: i8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedAdd", @@ -2467,7 +2471,7 @@ }, { "operation": "Intrinsic.i8CheckedSubtract", - "signature": "fn Intrinsic.i8CheckedSubtract(left: i8, right: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedSubtract(left: i8, right: i8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedSubtract", @@ -2475,7 +2479,7 @@ }, { "operation": "Intrinsic.i8CheckedMultiply", - "signature": "fn Intrinsic.i8CheckedMultiply(left: i8, right: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedMultiply(left: i8, right: i8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedMultiply", @@ -2483,7 +2487,7 @@ }, { "operation": "Intrinsic.i8CheckedDivide", - "signature": "fn Intrinsic.i8CheckedDivide(left: i8, right: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedDivide(left: i8, right: i8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedDivide", @@ -2491,7 +2495,7 @@ }, { "operation": "Intrinsic.i8CheckedRemainder", - "signature": "fn Intrinsic.i8CheckedRemainder(left: i8, right: i8) -> Option", + "signature": "fn Intrinsic.i8CheckedRemainder(left: i8, right: i8, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i8.checkedRemainder", @@ -2579,7 +2583,7 @@ }, { "operation": "Intrinsic.i16CheckedToU8", - "signature": "fn Intrinsic.i16CheckedToU8(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToU8(value: i16, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToU8", @@ -2595,7 +2599,7 @@ }, { "operation": "Intrinsic.i16CheckedToU16", - "signature": "fn Intrinsic.i16CheckedToU16(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToU16(value: i16, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToU16", @@ -2611,7 +2615,7 @@ }, { "operation": "Intrinsic.i16CheckedToU32", - "signature": "fn Intrinsic.i16CheckedToU32(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToU32(value: i16, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToU32", @@ -2627,7 +2631,7 @@ }, { "operation": "Intrinsic.i16CheckedToU64", - "signature": "fn Intrinsic.i16CheckedToU64(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToU64(value: i16, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToU64", @@ -2643,7 +2647,7 @@ }, { "operation": "Intrinsic.i16CheckedToUsize", - "signature": "fn Intrinsic.i16CheckedToUsize(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToUsize(value: i16, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToUsize", @@ -2659,7 +2663,7 @@ }, { "operation": "Intrinsic.i16CheckedToI8", - "signature": "fn Intrinsic.i16CheckedToI8(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToI8(value: i16, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToI8", @@ -2675,7 +2679,7 @@ }, { "operation": "Intrinsic.i16CheckedToI16", - "signature": "fn Intrinsic.i16CheckedToI16(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToI16(value: i16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToI16", @@ -2691,7 +2695,7 @@ }, { "operation": "Intrinsic.i16CheckedToI32", - "signature": "fn Intrinsic.i16CheckedToI32(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToI32(value: i16, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToI32", @@ -2707,7 +2711,7 @@ }, { "operation": "Intrinsic.i16CheckedToI64", - "signature": "fn Intrinsic.i16CheckedToI64(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToI64(value: i16, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToI64", @@ -2723,7 +2727,7 @@ }, { "operation": "Intrinsic.i16CheckedToIsize", - "signature": "fn Intrinsic.i16CheckedToIsize(value: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedToIsize(value: i16, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedToIsize", @@ -2899,7 +2903,7 @@ }, { "operation": "Intrinsic.i16CheckedAdd", - "signature": "fn Intrinsic.i16CheckedAdd(left: i16, right: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedAdd(left: i16, right: i16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedAdd", @@ -2907,7 +2911,7 @@ }, { "operation": "Intrinsic.i16CheckedSubtract", - "signature": "fn Intrinsic.i16CheckedSubtract(left: i16, right: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedSubtract(left: i16, right: i16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedSubtract", @@ -2915,7 +2919,7 @@ }, { "operation": "Intrinsic.i16CheckedMultiply", - "signature": "fn Intrinsic.i16CheckedMultiply(left: i16, right: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedMultiply(left: i16, right: i16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedMultiply", @@ -2923,7 +2927,7 @@ }, { "operation": "Intrinsic.i16CheckedDivide", - "signature": "fn Intrinsic.i16CheckedDivide(left: i16, right: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedDivide(left: i16, right: i16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedDivide", @@ -2931,7 +2935,7 @@ }, { "operation": "Intrinsic.i16CheckedRemainder", - "signature": "fn Intrinsic.i16CheckedRemainder(left: i16, right: i16) -> Option", + "signature": "fn Intrinsic.i16CheckedRemainder(left: i16, right: i16, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i16.checkedRemainder", @@ -3019,7 +3023,7 @@ }, { "operation": "Intrinsic.i32CheckedToU8", - "signature": "fn Intrinsic.i32CheckedToU8(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToU8(value: i32, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToU8", @@ -3035,7 +3039,7 @@ }, { "operation": "Intrinsic.i32CheckedToU16", - "signature": "fn Intrinsic.i32CheckedToU16(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToU16(value: i32, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToU16", @@ -3051,7 +3055,7 @@ }, { "operation": "Intrinsic.i32CheckedToU32", - "signature": "fn Intrinsic.i32CheckedToU32(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToU32(value: i32, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToU32", @@ -3067,7 +3071,7 @@ }, { "operation": "Intrinsic.i32CheckedToU64", - "signature": "fn Intrinsic.i32CheckedToU64(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToU64(value: i32, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToU64", @@ -3083,7 +3087,7 @@ }, { "operation": "Intrinsic.i32CheckedToUsize", - "signature": "fn Intrinsic.i32CheckedToUsize(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToUsize(value: i32, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToUsize", @@ -3099,7 +3103,7 @@ }, { "operation": "Intrinsic.i32CheckedToI8", - "signature": "fn Intrinsic.i32CheckedToI8(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToI8(value: i32, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToI8", @@ -3115,7 +3119,7 @@ }, { "operation": "Intrinsic.i32CheckedToI16", - "signature": "fn Intrinsic.i32CheckedToI16(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToI16(value: i32, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToI16", @@ -3131,7 +3135,7 @@ }, { "operation": "Intrinsic.i32CheckedToI32", - "signature": "fn Intrinsic.i32CheckedToI32(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToI32(value: i32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToI32", @@ -3147,7 +3151,7 @@ }, { "operation": "Intrinsic.i32CheckedToI64", - "signature": "fn Intrinsic.i32CheckedToI64(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToI64(value: i32, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToI64", @@ -3163,7 +3167,7 @@ }, { "operation": "Intrinsic.i32CheckedToIsize", - "signature": "fn Intrinsic.i32CheckedToIsize(value: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedToIsize(value: i32, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedToIsize", @@ -3339,7 +3343,7 @@ }, { "operation": "Intrinsic.i32CheckedAdd", - "signature": "fn Intrinsic.i32CheckedAdd(left: i32, right: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedAdd(left: i32, right: i32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedAdd", @@ -3347,7 +3351,7 @@ }, { "operation": "Intrinsic.i32CheckedSubtract", - "signature": "fn Intrinsic.i32CheckedSubtract(left: i32, right: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedSubtract(left: i32, right: i32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedSubtract", @@ -3355,7 +3359,7 @@ }, { "operation": "Intrinsic.i32CheckedMultiply", - "signature": "fn Intrinsic.i32CheckedMultiply(left: i32, right: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedMultiply(left: i32, right: i32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedMultiply", @@ -3363,7 +3367,7 @@ }, { "operation": "Intrinsic.i32CheckedDivide", - "signature": "fn Intrinsic.i32CheckedDivide(left: i32, right: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedDivide(left: i32, right: i32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedDivide", @@ -3371,7 +3375,7 @@ }, { "operation": "Intrinsic.i32CheckedRemainder", - "signature": "fn Intrinsic.i32CheckedRemainder(left: i32, right: i32) -> Option", + "signature": "fn Intrinsic.i32CheckedRemainder(left: i32, right: i32, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i32.checkedRemainder", @@ -3459,7 +3463,7 @@ }, { "operation": "Intrinsic.i64CheckedToU8", - "signature": "fn Intrinsic.i64CheckedToU8(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToU8(value: i64, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToU8", @@ -3475,7 +3479,7 @@ }, { "operation": "Intrinsic.i64CheckedToU16", - "signature": "fn Intrinsic.i64CheckedToU16(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToU16(value: i64, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToU16", @@ -3491,7 +3495,7 @@ }, { "operation": "Intrinsic.i64CheckedToU32", - "signature": "fn Intrinsic.i64CheckedToU32(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToU32(value: i64, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToU32", @@ -3507,7 +3511,7 @@ }, { "operation": "Intrinsic.i64CheckedToU64", - "signature": "fn Intrinsic.i64CheckedToU64(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToU64(value: i64, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToU64", @@ -3523,7 +3527,7 @@ }, { "operation": "Intrinsic.i64CheckedToUsize", - "signature": "fn Intrinsic.i64CheckedToUsize(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToUsize(value: i64, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToUsize", @@ -3539,7 +3543,7 @@ }, { "operation": "Intrinsic.i64CheckedToI8", - "signature": "fn Intrinsic.i64CheckedToI8(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToI8(value: i64, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToI8", @@ -3555,7 +3559,7 @@ }, { "operation": "Intrinsic.i64CheckedToI16", - "signature": "fn Intrinsic.i64CheckedToI16(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToI16(value: i64, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToI16", @@ -3571,7 +3575,7 @@ }, { "operation": "Intrinsic.i64CheckedToI32", - "signature": "fn Intrinsic.i64CheckedToI32(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToI32(value: i64, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToI32", @@ -3587,7 +3591,7 @@ }, { "operation": "Intrinsic.i64CheckedToI64", - "signature": "fn Intrinsic.i64CheckedToI64(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToI64(value: i64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToI64", @@ -3603,7 +3607,7 @@ }, { "operation": "Intrinsic.i64CheckedToIsize", - "signature": "fn Intrinsic.i64CheckedToIsize(value: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedToIsize(value: i64, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedToIsize", @@ -3779,7 +3783,7 @@ }, { "operation": "Intrinsic.i64CheckedAdd", - "signature": "fn Intrinsic.i64CheckedAdd(left: i64, right: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedAdd(left: i64, right: i64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedAdd", @@ -3787,7 +3791,7 @@ }, { "operation": "Intrinsic.i64CheckedSubtract", - "signature": "fn Intrinsic.i64CheckedSubtract(left: i64, right: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedSubtract(left: i64, right: i64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedSubtract", @@ -3795,7 +3799,7 @@ }, { "operation": "Intrinsic.i64CheckedMultiply", - "signature": "fn Intrinsic.i64CheckedMultiply(left: i64, right: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedMultiply(left: i64, right: i64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedMultiply", @@ -3803,7 +3807,7 @@ }, { "operation": "Intrinsic.i64CheckedDivide", - "signature": "fn Intrinsic.i64CheckedDivide(left: i64, right: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedDivide(left: i64, right: i64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedDivide", @@ -3811,7 +3815,7 @@ }, { "operation": "Intrinsic.i64CheckedRemainder", - "signature": "fn Intrinsic.i64CheckedRemainder(left: i64, right: i64) -> Option", + "signature": "fn Intrinsic.i64CheckedRemainder(left: i64, right: i64, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/i64.checkedRemainder", @@ -3899,7 +3903,7 @@ }, { "operation": "Intrinsic.isizeCheckedToU8", - "signature": "fn Intrinsic.isizeCheckedToU8(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToU8(value: isize, present: once fn(u8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToU8", @@ -3915,7 +3919,7 @@ }, { "operation": "Intrinsic.isizeCheckedToU16", - "signature": "fn Intrinsic.isizeCheckedToU16(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToU16(value: isize, present: once fn(u16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToU16", @@ -3931,7 +3935,7 @@ }, { "operation": "Intrinsic.isizeCheckedToU32", - "signature": "fn Intrinsic.isizeCheckedToU32(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToU32(value: isize, present: once fn(u32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToU32", @@ -3947,7 +3951,7 @@ }, { "operation": "Intrinsic.isizeCheckedToU64", - "signature": "fn Intrinsic.isizeCheckedToU64(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToU64(value: isize, present: once fn(u64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToU64", @@ -3963,7 +3967,7 @@ }, { "operation": "Intrinsic.isizeCheckedToUsize", - "signature": "fn Intrinsic.isizeCheckedToUsize(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToUsize(value: isize, present: once fn(usize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToUsize", @@ -3979,7 +3983,7 @@ }, { "operation": "Intrinsic.isizeCheckedToI8", - "signature": "fn Intrinsic.isizeCheckedToI8(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToI8(value: isize, present: once fn(i8) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToI8", @@ -3995,7 +3999,7 @@ }, { "operation": "Intrinsic.isizeCheckedToI16", - "signature": "fn Intrinsic.isizeCheckedToI16(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToI16(value: isize, present: once fn(i16) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToI16", @@ -4011,7 +4015,7 @@ }, { "operation": "Intrinsic.isizeCheckedToI32", - "signature": "fn Intrinsic.isizeCheckedToI32(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToI32(value: isize, present: once fn(i32) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToI32", @@ -4027,7 +4031,7 @@ }, { "operation": "Intrinsic.isizeCheckedToI64", - "signature": "fn Intrinsic.isizeCheckedToI64(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToI64(value: isize, present: once fn(i64) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToI64", @@ -4043,7 +4047,7 @@ }, { "operation": "Intrinsic.isizeCheckedToIsize", - "signature": "fn Intrinsic.isizeCheckedToIsize(value: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedToIsize(value: isize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedToIsize", @@ -4219,7 +4223,7 @@ }, { "operation": "Intrinsic.isizeCheckedAdd", - "signature": "fn Intrinsic.isizeCheckedAdd(left: isize, right: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedAdd(left: isize, right: isize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedAdd", @@ -4227,7 +4231,7 @@ }, { "operation": "Intrinsic.isizeCheckedSubtract", - "signature": "fn Intrinsic.isizeCheckedSubtract(left: isize, right: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedSubtract(left: isize, right: isize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedSubtract", @@ -4235,7 +4239,7 @@ }, { "operation": "Intrinsic.isizeCheckedMultiply", - "signature": "fn Intrinsic.isizeCheckedMultiply(left: isize, right: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedMultiply(left: isize, right: isize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedMultiply", @@ -4243,7 +4247,7 @@ }, { "operation": "Intrinsic.isizeCheckedDivide", - "signature": "fn Intrinsic.isizeCheckedDivide(left: isize, right: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedDivide(left: isize, right: isize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedDivide", @@ -4251,7 +4255,7 @@ }, { "operation": "Intrinsic.isizeCheckedRemainder", - "signature": "fn Intrinsic.isizeCheckedRemainder(left: isize, right: isize) -> Option", + "signature": "fn Intrinsic.isizeCheckedRemainder(left: isize, right: isize, present: once fn(isize) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/isize.checkedRemainder", @@ -4883,7 +4887,7 @@ }, { "operation": "Intrinsic.charFromU32", - "signature": "fn Intrinsic.charFromU32(value: u32) -> Option", + "signature": "fn Intrinsic.charFromU32(value: u32, present: once fn(char) -> R, absent: once fn() -> R) -> R", "unsafe": false, "admission": "Scalar", "consumer": "silk/char.fromU32", @@ -5043,7 +5047,7 @@ }, { "operation": "Intrinsic.osFileRead", - "signature": "unsafe fn Intrinsic.osFileRead(handle: &mut OsHandle, output: &mut [u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osFileRead(handle: &mut OsHandle, output: &mut [u8], count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_filesystem.fileRead", @@ -5052,7 +5056,7 @@ }, { "operation": "Intrinsic.osFileWrite", - "signature": "unsafe fn Intrinsic.osFileWrite(handle: &mut OsHandle, input: &[u8], offset: usize, reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osFileWrite(handle: &mut OsHandle, input: &[u8], offset: usize, count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_filesystem.fileWrite", @@ -5070,7 +5074,7 @@ }, { "operation": "Intrinsic.osDirectoryNext", - "signature": "unsafe fn Intrinsic.osDirectoryNext(handle: &mut OsHandle, output: &mut [u8], kind: &mut i32, requiredCapacity: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osDirectoryNext(handle: &mut OsHandle, output: &mut [u8], count: &mut usize, kind: &mut i32, requiredCapacity: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_filesystem.directoryNext", @@ -5088,7 +5092,7 @@ }, { "operation": "Intrinsic.osDirectoryCreateUnique", - "signature": "unsafe fn Intrinsic.osDirectoryCreateUnique(root: &[u8], parent: &[u8], prefix: &[u8], output: &mut [u8], requiredCapacity: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osDirectoryCreateUnique(root: &[u8], parent: &[u8], prefix: &[u8], output: &mut [u8], count: &mut usize, requiredCapacity: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_filesystem.directoryCreateUnique", @@ -5133,7 +5137,7 @@ }, { "operation": "Intrinsic.osStandardInputRead", - "signature": "unsafe fn Intrinsic.osStandardInputRead(output: &mut [u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osStandardInputRead(output: &mut [u8], count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_standard_input.read", @@ -5151,7 +5155,7 @@ }, { "operation": "Intrinsic.osProcessCapture", - "signature": "unsafe fn Intrinsic.osProcessCapture(stream: i32, offset: usize, output: &mut [u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osProcessCapture(stream: i32, offset: usize, output: &mut [u8], count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_child_process.capture", @@ -5169,7 +5173,7 @@ }, { "operation": "Intrinsic.osHostArgument", - "signature": "unsafe fn Intrinsic.osHostArgument(index: usize, output: &mut [u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osHostArgument(index: usize, output: &mut [u8], count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_host_input.argument", @@ -5178,7 +5182,7 @@ }, { "operation": "Intrinsic.osHostVariable", - "signature": "unsafe fn Intrinsic.osHostVariable(name: &[u8], output: &mut [u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osHostVariable(name: &[u8], output: &mut [u8], count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_host_input.variable", @@ -5187,7 +5191,7 @@ }, { "operation": "Intrinsic.osHostWorkingDirectory", - "signature": "unsafe fn Intrinsic.osHostWorkingDirectory(output: &mut [u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osHostWorkingDirectory(output: &mut [u8], count: &mut usize, reason: &mut i32, nativeCode: &mut u32) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_host_input.workingDirectory", @@ -5418,7 +5422,7 @@ }, { "operation": "Intrinsic.effectResult", - "signature": "fn Intrinsic.effectResult(protected: Effect) -> Effect ? R>", + "signature": "fn Intrinsic.effectResult(protected: Effect, success: once fn(A) -> R, failure: once fn(E) -> R) -> Effect", "unsafe": false, "admission": "Effect", "consumer": "silk/effect.result", From 3ec073a7f6b3591df87252fab5f40775104b2d0d Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 16:09:05 -0300 Subject: [PATCH 15/42] spec: compose Effect.result from general effects --- openspec/changes/add-nominal-unions/design.md | 35 ++++++++++--------- .../specs/bootstrap-flow-functions/spec.md | 26 ++++++++++---- .../bootstrap-intrinsic-boundary/spec.md | 20 +++++------ openspec/changes/add-nominal-unions/tasks.md | 4 +-- 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/openspec/changes/add-nominal-unions/design.md b/openspec/changes/add-nominal-unions/design.md index d08347abd..b9cc7766c 100644 --- a/openspec/changes/add-nominal-unions/design.md +++ b/openspec/changes/add-nominal-unions/design.md @@ -271,19 +271,18 @@ The selected callback is invoked exactly once; the unused callable environment i Integer wrappers pass `some` and `none`, so public operations still return `Option`, while an equivalent user wrapper may choose another carrier without compiler registration. -Completed Effect reification similarly becomes a carrier-neutral fold: - -```text -effectOutcome( - protected: once Effect, - success: once fn(A) -> B, - failure: once fn(E) -> B, -) -> B ? R -``` - -`Effect.result` passes ordinary `succeed` and `failResult` functions. The primitive -preserves lazy timing, access, ownership, cleanup, requirements, and future suspension, but contains -no Result identity. +Completed Effect reification needs no intrinsic. Ordinary Silk `Effect.result` first maps the +protected success through a `once fn(A) -> Result` constructor for `Result.Success`, then uses +the general `Effect.catchAll` operation with a `once fn(E) -> Result` handler for +`Result.Failure`. `catchAll` already selects the complete typed failure value, including a normalized +structural union such as `HttpError | OutOfMemoryError`, while preserving the protected requirement +row, lazy timing, access, ownership, cleanup, and future suspension behavior. Both callbacks are +ordinary exact callables, so the same composition can target any user-defined result-like nominal +union without compiler registration. + +The compiler therefore removes the temporary `Intrinsic.effectResult` operation and its dedicated +analysis, HIR, MIR, evaluator, Wasm, LLVM, and suspension-metadata paths. No compatibility alias or +replacement completed-outcome primitive remains. Unsafe host primitives that only report counts use a `bool` result plus explicit initialized count/reason/code outputs. Handle-producing file and directory opens cannot use an optional handle @@ -294,10 +293,12 @@ to the selected callback; failure creates no handle. Ordinary source then constr data. This removes Option from low-level OS, standard-input, child-process, and process-input contracts without adding partial initialization semantics. -Alternative rejected: resolve canonical `silk.option.Option` or `silk.result.Result` inside compiler -phases. Even if the lookup used a declaration index, the compiler would still grant library identity -by module/name spelling and would retain the abstraction-shaped privilege this migration is meant to -remove. +Alternatives rejected: resolve canonical `silk.option.Option` or `silk.result.Result` inside compiler +phases, or retain a carrier-neutral completed-outcome fold merely because it avoids those names. The +former grants library identity by spelling; the latter duplicates the already-general composition of +`map` and `catchAll` and leaves unnecessary compiler privilege. Checked scalar and affine host-open +carriers remain justified because they expose primitive success information that ordinary Silk cannot +otherwise observe. ### 11. Option and Result migrate atomically after compiler support is complete diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md index 229cf9ec7..d2a77c887 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-flow-functions/spec.md @@ -5,12 +5,14 @@ Canonical ordinary Silk `Effect.result` SHALL execute exactly one Effect layer and reify its completed typed outcome as direct ordinary nominal `Result` data instead of propagating `E`. It SHALL construct `Result.Success` or `Result.Failure` without a wrapper field, -detached member, or intermediate structural union. Its implementation MAY wrap the minimum sealed -Effect primitive needed to distinguish a completed success from a typed failure, but the compiler -MUST NOT recognize `Result`, its module, or either variant by spelling. The operation SHALL preserve -`R`, ownership, cleanup, run access, and lazy timing, and its contract SHALL remain valid if execution -can suspend before producing the Result in a future runtime. Traps and future interruption MUST NOT -be converted into typed `E` values. +detached member, or intermediate structural union. Its implementation SHALL first map the protected +success through an ordinary exact `once fn(A) -> Result` constructor, then apply the general +`Effect.catchAll` operation with an ordinary exact `once fn(E) -> Result` handler for the +complete typed failure value. It MUST NOT use a completed-outcome intrinsic, and the compiler MUST +NOT recognize `Result`, its module, or either variant by spelling. The composition SHALL preserve `R`, +ownership, cleanup, run access, and lazy timing, and its contract SHALL remain valid if execution can +suspend before producing the Result in a future runtime. Traps and future interruption MUST NOT be +converted into typed `E` values. #### Scenario: Map both completed branches in library code @@ -29,5 +31,15 @@ be converted into typed `E` values. #### Scenario: Rename an equivalent source wrapper -- **WHEN** equivalent ordinary source wraps the same minimal Effect primitive under another legal function name +- **WHEN** equivalent ordinary source maps success and catches failure into a user-defined result-like nominal union under another legal function name - **THEN** it can construct and return a user-selected nominal union without compiler registration of that union or its variants + +#### Scenario: Reify a compound failure union + +- **WHEN** the protected Effect can fail with `HttpError | OutOfMemoryError` +- **THEN** `catchAll` passes that complete structural union to the Failure constructor without flattening the outer Result or losing either error alternative + +#### Scenario: Preserve affine branch values + +- **WHEN** either completed branch carries a move-only value and its constructor is an exact `once fn` +- **THEN** ordinary `map` and `catchAll` transfer that value exactly once and clean only the unselected callable environment diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md index ef7c13cc9..e13f11260 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md @@ -4,27 +4,27 @@ No intrinsic contract SHALL name, construct, match, or recognize source-defined `Option`, `Result`, or their variants. Existing checked scalar primitives SHALL receive ordinary present and absent -carrier inputs and return their shared result type. Existing completed-Effect reification SHALL -receive ordinary success and failure carrier functions and return their shared result type while -preserving its requirement row. The inventory, semantic analysis, HIR, MIR, evaluation, and every -backend SHALL treat those carriers through their ordinary exact callable and value contracts. This -change SHALL replace the abstraction-shaped existing signatures and SHALL add no new source-callable -intrinsic operation. +carrier inputs and return their shared result type. The inventory, semantic analysis, HIR, MIR, +evaluation, and every backend SHALL treat those carriers through their ordinary exact callable and +value contracts. Completed Effect outcomes SHALL be handled by ordinary Effect composition rather +than an intrinsic. This change SHALL replace the abstraction-shaped existing signatures, SHALL remove +`Intrinsic.effectResult` and all of its compiler support, and SHALL add no replacement source-callable +operation. #### Scenario: Construct Option in an integer wrapper - **WHEN** an ordinary checked-integer wrapper supplies the ordinary `some` and `none` constructor functions to its scalar primitive - **THEN** the primitive selects the correct ordinary carrier and contains no canonical Option or variant identity -#### Scenario: Construct Result in Effect.result +#### Scenario: Keep completed Effect reification out of Intrinsic -- **WHEN** ordinary `Effect.result` supplies the ordinary `succeed` and `failResult` constructor functions to completed-outcome reification -- **THEN** the primitive invokes exactly one carrier and contains no canonical Result or variant identity +- **WHEN** ordinary `Effect.result` maps success and catches the complete typed failure in library code +- **THEN** the intrinsic inventory contains no completed-outcome operation and the compiler contains no dedicated HIR, MIR, evaluator, or backend path for it #### Scenario: Audit the closed inventory - **WHEN** the intrinsic inventory is compared before and after migration -- **THEN** abstraction-shaped Option and Result result contracts are gone, no new callable operation exists, and every changed operation has one carrier-neutral contract +- **THEN** abstraction-shaped Option and Result result contracts are gone, `Intrinsic.effectResult` is absent, no replacement callable operation exists, and every remaining changed primitive has one carrier-neutral contract ## MODIFIED Requirements diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index ff5dcf04c..98e373502 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -69,7 +69,7 @@ - [ ] 9.1 Replace checked scalar intrinsic result contracts with generic present/absent exact `once fn` carriers while keeping the intrinsic operation inventory count unchanged, and verify catalog audit tests contain no Option identity or spelling. - [ ] 9.2 Lower and execute checked carrier selection with exactly one callback invocation and cleanup of the unused callable environment, and verify evaluator, Wasm, and native tests cover success, absence, affine captures, and traps. -- [ ] 9.3 Replace abstraction-shaped completed-Effect reification with a carrier-neutral success/failure fold preserving requirement rows, access, cleanup, laziness, and suspension, and verify an equivalent user wrapper can select another nominal carrier without compiler registration. +- [ ] 9.3 Delete `Intrinsic.effectResult` and its analysis, HIR, MIR, evaluator, Wasm, LLVM, layout-discovery, and suspension-metadata support without replacement, and verify the intrinsic inventory plus repository searches contain no completed-outcome primitive or compatibility path. - [ ] 9.4 Replace handle-producing file and directory open results with affine-safe success/failure `once fn` carriers, and verify success transfers one initialized `OsHandle` plus close obligation while failure creates no handle or optionally initialized place. - [ ] 9.5 Replace optional count-producing OS filesystem, standard-input, child-process, and process-input results with primitive `bool` plus initialized count/reason/code outputs, and verify host-boundary tests distinguish zero-length success, absence, and refusal without constructing Option in compiler code. - [ ] 9.6 Remove `Type.option`, old Result/member helpers, detached outcome construction, and Option/Result-specific branches from analysis, HIR, MIR, evaluation, and backends, and verify repository searches plus intrinsic audits find no compiler recognition by standard-library module or declaration spelling. @@ -79,7 +79,7 @@ - [ ] 10.1 Replace `option.silk` with the public nominal union and direct `some`/`none` helpers, and verify its combinators construct and match direct variants with public payload access and no wrapper field. - [ ] 10.2 Replace `result.silk` with the public nominal union and direct `succeed`/`failResult` helpers, and verify its combinators accept structural error unions without flattening Success or Failure. - [ ] 10.3 Update integer, character, string, allocation, and other checked wrappers to supply carrier-neutral intrinsic adapters and return direct Option variants, and verify checked success/absence tests use the canonical nominal representation. -- [ ] 10.4 Update `Effect.result`, Effect combinators, and every direct intrinsic outcome consumer to construct and match direct Result variants, and verify success/failure reification has exactly one nominal layer. +- [ ] 10.4 Implement ordinary Silk `Effect.result` by mapping success into `Result.Success` and applying general `Effect.catchAll` to map the complete typed failure into `Result.Failure`; migrate direct consumers and verify one nominal layer, compound failure unions, preserved requirements, move-only branch values, and an equivalent user-defined result-like union. - [ ] 10.5 Migrate filesystem, process, formatting, random, collection, and remaining canonical Silk modules from detached member imports and wrapper-field matches to qualified parent variants, and verify the complete stdlib source closure compiles. - [ ] 10.6 Delete detached `Some`, `None`, `Success`, and `Failure` declarations, wrapper structs, aliases, dual paths, stale imports, and old generated embeddings, then regenerate the deterministic stdlib manifest and verify a repository-wide removal test finds no superseded representation. From 4628fd97e8646bd1457f370436350382976be2a2 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 16:57:43 -0300 Subject: [PATCH 16/42] Implement Effect result through source composition --- packages/compiler/src/CallResolution.ts | 28 ++++- packages/compiler/src/Constraint.ts | 9 ++ packages/compiler/src/EffectLowering.ts | 109 +++++++++++++++++- packages/compiler/src/Instances.ts | 24 ++-- packages/compiler/src/Layout.ts | 4 + packages/compiler/src/LowerExpression.ts | 6 +- packages/compiler/src/RowAlgebra.ts | 16 +-- packages/compiler/src/Stdlib.generated.ts | 5 +- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/ValueType.ts | 1 + packages/compiler/stdlib/silk/effect.silk | 24 ++-- packages/compiler/test/ResultStdlib.test.ts | 54 +++++++++ packages/compiler/test/SelectiveCatch.test.ts | 50 +++++++- 13 files changed, 285 insertions(+), 47 deletions(-) diff --git a/packages/compiler/src/CallResolution.ts b/packages/compiler/src/CallResolution.ts index fd273c3c9..9b5be3a4a 100644 --- a/packages/compiler/src/CallResolution.ts +++ b/packages/compiler/src/CallResolution.ts @@ -1811,10 +1811,36 @@ export const analyzeFunctionItem = ( unresolvedCallable.schema, unresolvedCallable.unsafe, ) - const specialized = + let specialized = contextualPattern !== undefined && expectedCallable !== undefined && TypeInference.infer(contextualPattern, expectedCallable, contextual) + if (!specialized && contextualPattern !== undefined && expectedCallable !== undefined) { + const partial = new Map() + const parametersCompatible = + contextualPattern.parameters.length === expectedCallable.parameters.length && + contextualPattern.parameters.every((parameter, ordinal) => { + const expectedParameter = expectedCallable.parameters.at(ordinal) + return ( + expectedParameter !== undefined && + TypeInference.infer(parameter, expectedParameter, partial) + ) + }) + const patternResult = contextualPattern.result + const expectedResult = expectedCallable.result + const resultCompatible = + Type.isEffect(patternResult) && Type.isEffect(expectedResult) + ? TypeInference.infer(patternResult.success, expectedResult.success, partial) + : TypeInference.infer(patternResult, expectedResult, partial) + const allBindersDetermined = (contract?.binders ?? []).every((parameter) => + partial.has(Type.key(parameter)), + ) + if (parametersCompatible && resultCompatible && allBindersDetermined) { + contextual.clear() + for (const [key, argument] of partial) contextual.set(key, argument) + specialized = true + } + } let callable = unresolvedCallable if (callable !== undefined && specialized) { const contextualCallable = Type.substitute(callable, contextual) diff --git a/packages/compiler/src/Constraint.ts b/packages/compiler/src/Constraint.ts index 2fe2c16bd..bce297d30 100644 --- a/packages/compiler/src/Constraint.ts +++ b/packages/compiler/src/Constraint.ts @@ -252,6 +252,15 @@ export const proveStructural = ( return Object.freeze({ _tag: 'Member', selected: self.selected, source: self.source }) } case 'FailureSubsetConstraint': { + if ( + self.selected.expression._tag === 'Singleton' && + RowAlgebra.isKnownSubset(Type.failureRowPolicy(), self.selected, self.source) + ) + return Object.freeze({ + _tag: 'FailureSubset', + selected: self.selected, + source: self.source, + }) const selected = RowAlgebra.concretize(Type.failureRowPolicy(), self.selected) const source = RowAlgebra.concretize(Type.failureRowPolicy(), self.source) if ( diff --git a/packages/compiler/src/EffectLowering.ts b/packages/compiler/src/EffectLowering.ts index 4d1d49fb1..6012e418c 100644 --- a/packages/compiler/src/EffectLowering.ts +++ b/packages/compiler/src/EffectLowering.ts @@ -47,8 +47,9 @@ export const lowerCatchEffectValue = ( protectedType?._tag !== 'EffectValue' || handler === undefined || handlerType?._tag !== 'CallableValue' - ) + ) { return undefined + } const site = Hir.effectCatchSite( fn.owner.function.declaration.id, @@ -347,7 +348,8 @@ export const reifyEffectValue = ( boolType?._tag !== 'bool' || successType === undefined || successType._tag === 'EffectOutcome' || - (failureType?._tag !== 'Nominal' && failureType?._tag !== 'Union') || + failureType === undefined || + failureType._tag === 'EffectOutcome' || outcomeShape === undefined || successShape === undefined || failureValueShape === undefined || @@ -467,8 +469,54 @@ export const lowerEffectCatch = ( const selected = fn.semantic(expression.selected) const protectedEffect = fn.semantic(expression.protected.type) const resultEffect = fn.semantic(expression.type) - if (Type.isNever(selected) || !Type.isEffect(protectedEffect) || !Type.isEffect(resultEffect)) - return undefined + if (!Type.isEffect(protectedEffect) || !Type.isEffect(resultEffect)) return undefined + if (Type.isNever(selected)) { + const succeeded = lowerRunEffectValue( + fn, + protected_.result, + protectedType, + protectedEffect.success, + runSpan, + ) + if (succeeded === undefined) return undefined + for (const drop of unusedHandlerDrop()) fn.emit(drop) + if (Type.equals(protectedEffect.success, resultEffect.success)) { + endRunLoans(fn, runSpan) + return succeeded + } + const conversion = TypeCompatibility.check(protectedEffect.success, resultEffect.success) + const sourceType = fn.type(protectedEffect.success) + const targetType = fn.type(resultEffect.success) + const sourceShape = Layout.callingShape(fn.layout, protectedEffect.success) + const targetShape = Layout.callingShape(fn.layout, resultEffect.success) + if ( + conversion._tag !== 'Inject' || + sourceType === undefined || + sourceType._tag === 'EffectOutcome' || + targetType?._tag !== 'Union' || + sourceShape === undefined || + targetShape === undefined + ) + return undefined + const destination = fn.alloc(targetType) + fn.emit( + Object.freeze({ + _tag: 'ConvertUnion' as const, + destination, + source: succeeded.result, + sourceType, + targetType, + conversion: 'Inject' as const, + mappings: conversion.mappings, + sourceShape, + targetShape, + access: 'Owned' as const, + provenance: generated(expression.span), + }), + ) + endRunLoans(fn, runSpan) + return Object.freeze({ result: destination }) + } const protectedFailures = Type.failureMembers(protectedEffect) const selectedMembers: ReadonlyArray = Type.isUnion(selected) ? selected.members @@ -494,13 +542,64 @@ export const lowerEffectCatch = ( successType === undefined || successType._tag === 'EffectOutcome' || successShape === undefined || - (failureValueMir?._tag !== 'Nominal' && failureValueMir?._tag !== 'Union') || + failureValueMir === undefined || + failureValueMir._tag === 'EffectOutcome' || propagationEffect === undefined || propagationType?._tag !== 'EffectOutcome' || propagationShape === undefined ) return undefined + if (failureValueMir._tag !== 'Nominal' && failureValueMir._tag !== 'Union') { + const onlyFailure = protectedFailures.at(0) + if ( + protectedFailures.length !== 1 || + onlyFailure === undefined || + !selectedMembers.some((candidate) => Type.equals(candidate, onlyFailure)) + ) + return undefined + const [handled, handledOperations] = fn.capture(() => { + const applied = fn.alloc(handlerEffectType) + fn.emit( + Object.freeze({ + _tag: 'ApplyCallable' as const, + destination: applied, + callable: handler.result, + typeArguments: + handlerType.environment?.callable.typeArguments ?? + handlerType.storage?.realization.targetArguments ?? + handlerType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: Object.freeze([reified.failure]), + callableType: handlerType.type, + access: handlerType.type.mode, + evaluation: 'CalleeThenArguments' as const, + realization: 'Environment' as const, + type: handlerEffectType, + provenance: generated(expression.span), + }), + ) + return lowerRunEffectValue(fn, applied, handlerEffectType, resultEffect.success, runSpan) + }) + if (handled === undefined) return undefined + const destination = fn.alloc(successType) + fn.emit( + Object.freeze({ + _tag: 'Conditional' as const, + destination, + condition: reified.valid, + taken: Object.freeze({ operations: unusedHandlerDrop(), result: reified.success }), + otherwise: Object.freeze({ operations: handledOperations, result: handled.result }), + type: successType, + resultShape: successShape, + provenance: generated(expression.span), + }), + ) + endRunLoans(fn, runSpan) + return Object.freeze({ result: destination }) + } + const declaration = fn.owner.function.declaration.id const failureMembers = failureValueMir._tag === 'Nominal' diff --git a/packages/compiler/src/Instances.ts b/packages/compiler/src/Instances.ts index 2db858e55..a7cb759b6 100644 --- a/packages/compiler/src/Instances.ts +++ b/packages/compiler/src/Instances.ts @@ -415,15 +415,23 @@ const specializeEvidence = ( const source = Type.substituteFailureRow(evidence.source, substitution) return concreteConstraintEvidence(Constraint.nominalMember(selected, source), origin, index) } - if (evidence._tag === 'FailureSubset') - return concreteConstraintEvidence( - Constraint.failureSubset( - Type.substituteFailureRow(evidence.selected, substitution), - Type.substituteFailureRow(evidence.source, substitution), - ), - origin, - index, + if (evidence._tag === 'FailureSubset') { + const selected = Type.substituteFailureRow(evidence.selected, substitution) + const source = Type.substituteFailureRow(evidence.source, substitution) + const selectedConcrete = RowAlgebra.concretize(Type.failureRowPolicy(), selected) + const sourceConcrete = RowAlgebra.concretize(Type.failureRowPolicy(), source) + if ( + selectedConcrete._tag !== 'Concrete' || + sourceConcrete._tag !== 'Concrete' || + selectedConcrete.row.members.some((member) => !Type.isRuntimeConcrete(member)) || + sourceConcrete.row.members.some((member) => !Type.isRuntimeConcrete(member)) || + !RowAlgebra.isKnownSubset(Type.failureRowPolicy(), selected, source) ) + return undefined + return Object.freeze([ + Object.freeze({ _tag: 'FailureSubset', selected, source }), + ]) + } if (evidence._tag === 'RequirementSubset') return concreteConstraintEvidence( Constraint.requirementSubset( diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 7212465d4..1d0855a9b 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -2838,6 +2838,7 @@ export const plan = ( for (const expression of instance.function.statements .flatMap(Hir.statementExpressions) .flatMap(Hir.expressionTree)) { + if (expression._tag === 'EffectCatch') reached.set(Type.key('bool'), 'bool') if (expression._tag === 'EffectResult') { reached.set(Type.key('bool'), 'bool') const result = Type.substitute(expression.type, instance.substitution) @@ -2916,6 +2917,9 @@ export const plan = ( for (const field of candidate.executable?.fields ?? []) add(field.type) if (candidate.representation._tag === 'Aggregate') { for (const field of candidate.representation.fields) add(field.type) + } else if (candidate.representation._tag === 'NominalUnion') { + for (const variant of candidate.representation.variants) + for (const field of variant.fields) add(field.type) } else if ( candidate.representation._tag === 'CallableEnvironment' || candidate.representation._tag === 'StoredEffectEnvironment' diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index d062aade5..88fc298c9 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -526,7 +526,11 @@ export function lowerExpressionInner( if (lowered === undefined || loweredType?._tag !== 'CallableValue') return false callable = lowered.result callableType = loweredType.type - typeArguments = loweredType.environment?.callable.typeArguments ?? Object.freeze([]) + typeArguments = + loweredType.environment?.callable.typeArguments ?? + loweredType.storage?.realization.targetArguments ?? + loweredType.typeArguments ?? + Object.freeze([]) return true } const lowered = diff --git a/packages/compiler/src/RowAlgebra.ts b/packages/compiler/src/RowAlgebra.ts index 6a0b979d0..405a13d84 100644 --- a/packages/compiler/src/RowAlgebra.ts +++ b/packages/compiler/src/RowAlgebra.ts @@ -321,22 +321,10 @@ export const isKnownSubset = prove(operand, right)) + if (right._tag === 'Union') return right.operands.some((operand) => prove(left, operand)) if (left._tag === 'Concrete' && right._tag === 'Concrete') return FiniteRow.isSubset(policy.finite, left.row, right.row) - if (right._tag !== 'Union') return false - if (left._tag === 'Concrete') { - const concreteContainer = right.operands.find( - (operand): operand is Extract => - operand._tag === 'Concrete', - ) - return ( - concreteContainer !== undefined && - FiniteRow.isSubset(policy.finite, left.row, concreteContainer.row) - ) - } - return right.operands.some( - (operand) => expressionKey(policy, left) === expressionKey(policy, operand), - ) + return false } return prove(candidate.expression, container.expression) } diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index fbec6134d..a029c4527 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -96,7 +96,7 @@ export const modules = [ module: 'silk/effect', path: 'silk/effect.silk', sourceIdentity: 'silk/effect', - digest: '1960eaa39497fa28a083d29ee7b5c4dacc74df2e7819a666d79c30e114721e5b', + digest: '70c150ae4e1d7c85ec9660f669a349a746401cdfb4e56bf3d3f73b48ad23f20a', documentation: 'silk/effect.silk', layer: 'portable', runtimeInventory: [ @@ -104,12 +104,11 @@ export const modules = [ 'bindRequirementMut', 'bindRequirementOwned', 'catchFailure', - 'effectResult', 'suspendEffect', ], namespace: 'Effect', source: - "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n return run Intrinsic.effectResult>(move protected, succeed, failResult)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run onFailure(move error)\n }\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", + "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", }, { module: 'silk/execution', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 7f41e7a49..95eee04bc 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '94db0903f9d0f21b1f8fe1a3840da001d3a72fb515beefa1f5abcd2ced70e181' +export const compilerDigest = '309f8978492aca399dd0e3d562322755aecfe2d16d9457985b8ef046d5047706' diff --git a/packages/compiler/src/ValueType.ts b/packages/compiler/src/ValueType.ts index 2627acfcc..863ea38ac 100644 --- a/packages/compiler/src/ValueType.ts +++ b/packages/compiler/src/ValueType.ts @@ -213,6 +213,7 @@ export const callableValueByIdentity = ( _tag: 'CallableValue', type: specializedType, target, + typeArguments: identity.typeArguments, ...(environment === undefined ? {} : { site: environment.callable.site, environment }), }) } diff --git a/packages/compiler/stdlib/silk/effect.silk b/packages/compiler/stdlib/silk/effect.silk index 640f200d4..d20430bd9 100644 --- a/packages/compiler/stdlib/silk/effect.silk +++ b/packages/compiler/stdlib/silk/effect.silk @@ -229,13 +229,22 @@ pub effect fn logError( pub effect fn result( protected: once Effect ) -> Result ? R { - return run Intrinsic.effectResult>(move protected, succeed, failResult) + let succeeded = map, E>(move protected, succeedCompleted) + return run catchAll, Result, E, never>(move succeeded, failCompleted) } effect fn raise(error: E) -> never ! E { fail move error } +fn succeedCompleted(value: A) -> Result { + return succeed(move value) +} + +effect fn failCompleted(error: E) -> Result { + return failResult(move error) +} + /// Transforms both possible typed outcomes with pure callbacks. /// /// # Details @@ -264,11 +273,8 @@ pub effect fn map( self: once Effect, onSuccess: once fn(A) -> B ) -> B ! E ? R { - let completed = run result(move self) - return match move completed { - Result.Success { value: success } => onSuccess(move success) - Result.Failure { error } => run raise(move error) - } + let success = run move self + return onSuccess(move success) } /// Applies a pure callback to typed failure while preserving success and requirements. @@ -406,11 +412,7 @@ pub effect fn catchAll( self: once Effect, onFailure: once fn(E) -> Effect ) -> A | B ! F ? R | S { - let completed = run result(move self) - return match move completed { - Result.Success { value: success } => move success - Result.Failure { error } => run onFailure(move error) - } + return run Intrinsic.catchFailure(move self, move onFailure) } /// Recovers one selected typed failure. diff --git a/packages/compiler/test/ResultStdlib.test.ts b/packages/compiler/test/ResultStdlib.test.ts index ea22ce6a6..90a2ae55f 100644 --- a/packages/compiler/test/ResultStdlib.test.ts +++ b/packages/compiler/test/ResultStdlib.test.ts @@ -122,6 +122,56 @@ fn release(storage: Allocation) -> i32 { pub fn main() -> i32 { return run build() }` +const alternateResultLikeUnion = `import silk.effect as Effect + +union Outcome { + Good { value: A }, + Bad { error: E }, +} + +struct First { code: i32 } +struct Second { code: i32 } + +fn good(value: A) -> Outcome { + return Outcome.Good { value: move value } +} + +effect fn bad(error: E) -> Outcome { + return Outcome.Bad { error: move error } +} + +effect fn outcome( + protected: once Effect +) -> Outcome ? R { + let succeeded = Effect.map, E>(move protected, good) + return run Effect.catchAll, Outcome, E, never>( + move succeeded, + bad + ) +} + +effect fn choose(first: bool) -> i32 ! First | Second { + if first { fail First { code: 20 } } + fail Second { code: 22 } +} + +effect fn inspect(first: bool) -> i32 { + let completed = run outcome(choose(first)) + return match move completed { + Outcome.Good { value } => value + Outcome.Bad { error } => match move error { + First { code } => code + Second { code } => code + } + } +} + +pub fn main() -> i32 { + let first = run inspect(true) + let second = run inspect(false) + return first + second +}` + const reifiedTrap = `import silk.effect as Effect import silk.result { Result } @@ -305,6 +355,10 @@ it.effect('preserves requirements while moving affine channel data into Result', evaluateAndRunWasm('reified-requirement', reifiedRequirement), ) +it.effect('composes an alternate generic result-like union from map and catchAll', () => + evaluateAndRunWasm('alternate-result-like-union', alternateResultLikeUnion), +) + it.effect('keeps runtime traps outside the typed Result error channel', () => Effect.gen(function* () { const snapshot = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/SelectiveCatch.test.ts b/packages/compiler/test/SelectiveCatch.test.ts index c91f583d4..06dc44f44 100644 --- a/packages/compiler/test/SelectiveCatch.test.ts +++ b/packages/compiler/test/SelectiveCatch.test.ts @@ -62,6 +62,18 @@ const replaceMirOperation = ( operations: Object.freeze(operation.right.operations.map(rewrite)), }), }) + if (operation._tag === 'Conditional') + return Object.freeze({ + ...operation, + taken: Object.freeze({ + ...operation.taken, + operations: Object.freeze(operation.taken.operations.map(rewrite)), + }), + otherwise: Object.freeze({ + ...operation.otherwise, + operations: Object.freeze(operation.otherwise.operations.map(rewrite)), + }), + }) if (operation._tag !== 'Match') return operation return Object.freeze({ ...operation, @@ -354,7 +366,9 @@ pub fn main() -> i32 { const self = yield* analyze(source, 'wasm32-unknown-unknown') assert.deepEqual(Analysis.diagnostics(self), []) const calls = self.instances.intrinsics.filter( - (call) => Intrinsic.operationText(call.operation) === 'Intrinsic.catchFailure', + (call) => + call.span.sourceId === 'root' && + Intrinsic.operationText(call.operation) === 'Intrinsic.catchFailure', ) assert.strictEqual(calls.length, 1) for (const target of Intrinsic.executionTargets) { @@ -460,7 +474,9 @@ pub fn main() -> i32 { return (run completed(true)) + (run completed(false)) } `) assert.deepEqual(Analysis.diagnostics(self), []) const calls = self.instances.intrinsics.filter( - (call) => Intrinsic.operationText(call.operation) === 'Intrinsic.catchFailure', + (call) => + call.span.sourceId === 'root' && + Intrinsic.operationText(call.operation) === 'Intrinsic.catchFailure', ) assert.strictEqual(calls.length, 1) for (const target of Intrinsic.executionTargets) @@ -537,7 +553,11 @@ pub fn main() -> i32 { return run Effect.catchAll(selective(true), recoverB) }`) .flatMap(Hir.statementExpressions) .flatMap((root) => [...Hir.expressionTree(root)]) .filter((expression) => expression._tag === 'EffectCatch') - .map((expression) => Object.freeze({ expression, substitution: instance.substitution })), + .map((expression) => Object.freeze({ expression, substitution: instance.substitution })) + .filter( + ({ expression, substitution }) => + Type.encode(Type.substitute(expression.selected, substitution)) === 'root.A', + ), ) assert.strictEqual(catches.length, 1) const found = catches.at(0) @@ -2001,6 +2021,18 @@ pub fn main() -> i32 { return run Effect.catchAll(selected(1), recoverAll) }`, operations: rewriteOperations(operation.right.operations, enclosing), }), }) + if (operation._tag === 'Conditional') + return Object.freeze({ + ...operation, + taken: Object.freeze({ + ...operation.taken, + operations: rewriteOperations(operation.taken.operations, enclosing), + }), + otherwise: Object.freeze({ + ...operation.otherwise, + operations: rewriteOperations(operation.otherwise.operations, enclosing), + }), + }) if (operation._tag === 'Match') { const source = operation.scrutineeType._tag === 'Union' @@ -2162,6 +2194,18 @@ pub fn main() -> i32 { return run Effect.catchAll(selected(1), recoverAll) }`, operations: injectStructuredPrefix(operation.right.operations, region), }), }) + if (operation._tag === 'Conditional') + return Object.freeze({ + ...operation, + taken: Object.freeze({ + ...operation.taken, + operations: injectStructuredPrefix(operation.taken.operations, region), + }), + otherwise: Object.freeze({ + ...operation.otherwise, + operations: injectStructuredPrefix(operation.otherwise.operations, region), + }), + }) if (operation._tag !== 'Match') return operation return Object.freeze({ ...operation, From 43e367186a860bd8452f786a4db387fc4e57ddee Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 17:14:34 -0300 Subject: [PATCH 17/42] Remove privileged Effect result lowering --- openspec/changes/add-nominal-unions/tasks.md | 4 +- packages/compiler/src/BootstrapEffect.ts | 8 +- packages/compiler/src/BootstrapEvaluation.ts | 4 +- packages/compiler/src/EffectLowering.ts | 188 +++-------------- packages/compiler/src/Elaboration.ts | 10 - packages/compiler/src/EntryAssembly.ts | 8 +- packages/compiler/src/ExecutableOrigin.ts | 74 +------ packages/compiler/src/ExpressionAnalysis.ts | 199 ------------------ packages/compiler/src/Hir.ts | 19 -- packages/compiler/src/HirLowering.ts | 23 -- .../compiler/src/InspectorProjectBackend.ts | 2 +- .../compiler/src/InspectorProjectSyntax.ts | 7 - packages/compiler/src/Intrinsic.ts | 44 ---- packages/compiler/src/Layout.ts | 23 +- .../src/LocalSharedAllocationProvenance.ts | 2 - packages/compiler/src/Lower.ts | 2 +- packages/compiler/src/LowerExpression.ts | 18 +- packages/compiler/src/LowerStatements.ts | 1 - packages/compiler/src/Mir.ts | 4 +- packages/compiler/src/MirEncoding.ts | 4 +- packages/compiler/src/MirLinearization.ts | 4 +- packages/compiler/src/MirNormalization.ts | 4 +- packages/compiler/src/MirVerification.ts | 14 +- packages/compiler/src/NativeCall.ts | 2 +- .../compiler/src/NativeEffectOperation.ts | 4 +- packages/compiler/src/NativeOperation.ts | 2 +- packages/compiler/src/NativeSuspension.ts | 4 +- packages/compiler/src/Ownership.ts | 38 ---- packages/compiler/src/ProvisionalMir.ts | 35 +-- packages/compiler/src/SemanticOccurrence.ts | 6 - packages/compiler/src/Stdlib.generated.ts | 4 +- packages/compiler/src/Suspension.ts | 4 +- packages/compiler/src/SuspensionMir.ts | 10 +- packages/compiler/src/SuspensionOwnership.ts | 4 +- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/Type.ts | 6 - packages/compiler/src/WasmBackend.ts | 16 +- packages/compiler/src/WasmMemory.ts | 2 +- packages/compiler/stdlib/silk/effect.silk | 10 +- packages/compiler/test/EffectRuntime.test.ts | 2 +- .../compiler/test/MirNormalization.test.ts | 50 ++--- packages/compiler/test/ZipAcceptance.test.ts | 2 +- .../test/fixtures/intrinsic-inventory.json | 8 - .../test/fixtures/synchronous-effect-cost.mjs | 2 +- packages/compiler/test/goldens/effect.mir.txt | Bin 10138 -> 4750 bytes 45 files changed, 128 insertions(+), 751 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 98e373502..ed115adfc 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -69,7 +69,7 @@ - [ ] 9.1 Replace checked scalar intrinsic result contracts with generic present/absent exact `once fn` carriers while keeping the intrinsic operation inventory count unchanged, and verify catalog audit tests contain no Option identity or spelling. - [ ] 9.2 Lower and execute checked carrier selection with exactly one callback invocation and cleanup of the unused callable environment, and verify evaluator, Wasm, and native tests cover success, absence, affine captures, and traps. -- [ ] 9.3 Delete `Intrinsic.effectResult` and its analysis, HIR, MIR, evaluator, Wasm, LLVM, layout-discovery, and suspension-metadata support without replacement, and verify the intrinsic inventory plus repository searches contain no completed-outcome primitive or compatibility path. +- [x] 9.3 Delete `Intrinsic.effectResult` and its analysis, HIR, MIR, evaluator, Wasm, LLVM, layout-discovery, and suspension-metadata support without replacement, and verify the intrinsic inventory plus repository searches contain no completed-outcome primitive or compatibility path. - [ ] 9.4 Replace handle-producing file and directory open results with affine-safe success/failure `once fn` carriers, and verify success transfers one initialized `OsHandle` plus close obligation while failure creates no handle or optionally initialized place. - [ ] 9.5 Replace optional count-producing OS filesystem, standard-input, child-process, and process-input results with primitive `bool` plus initialized count/reason/code outputs, and verify host-boundary tests distinguish zero-length success, absence, and refusal without constructing Option in compiler code. - [ ] 9.6 Remove `Type.option`, old Result/member helpers, detached outcome construction, and Option/Result-specific branches from analysis, HIR, MIR, evaluation, and backends, and verify repository searches plus intrinsic audits find no compiler recognition by standard-library module or declaration spelling. @@ -79,7 +79,7 @@ - [ ] 10.1 Replace `option.silk` with the public nominal union and direct `some`/`none` helpers, and verify its combinators construct and match direct variants with public payload access and no wrapper field. - [ ] 10.2 Replace `result.silk` with the public nominal union and direct `succeed`/`failResult` helpers, and verify its combinators accept structural error unions without flattening Success or Failure. - [ ] 10.3 Update integer, character, string, allocation, and other checked wrappers to supply carrier-neutral intrinsic adapters and return direct Option variants, and verify checked success/absence tests use the canonical nominal representation. -- [ ] 10.4 Implement ordinary Silk `Effect.result` by mapping success into `Result.Success` and applying general `Effect.catchAll` to map the complete typed failure into `Result.Failure`; migrate direct consumers and verify one nominal layer, compound failure unions, preserved requirements, move-only branch values, and an equivalent user-defined result-like union. +- [x] 10.4 Implement ordinary Silk `Effect.result` by mapping success into `Result.Success` and applying general `Effect.catchAll` to map the complete typed failure into `Result.Failure`; migrate direct consumers and verify one nominal layer, compound failure unions, preserved requirements, move-only branch values, and an equivalent user-defined result-like union. - [ ] 10.5 Migrate filesystem, process, formatting, random, collection, and remaining canonical Silk modules from detached member imports and wrapper-field matches to qualified parent variants, and verify the complete stdlib source closure compiles. - [ ] 10.6 Delete detached `Some`, `None`, `Success`, and `Failure` declarations, wrapper structs, aliases, dual paths, stale imports, and old generated embeddings, then regenerate the deterministic stdlib manifest and verify a repository-wide removal test finds no superseded representation. diff --git a/packages/compiler/src/BootstrapEffect.ts b/packages/compiler/src/BootstrapEffect.ts index 9458c72db..f0f35bcdd 100644 --- a/packages/compiler/src/BootstrapEffect.ts +++ b/packages/compiler/src/BootstrapEffect.ts @@ -238,7 +238,7 @@ export interface OperationContext { arguments_: ReadonlyArray, operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' } >, ) => Generator readonly callFunction: ( @@ -270,11 +270,11 @@ type EffectOperation = Extract< | 'RunEffectComposite' | 'RunEffectValue' | 'RunStaticEffect' - | 'ReifyEffect' + | 'CatchEffect' } > -/** Executes Effect construction, execution, propagation, and reification. */ +/** Executes Effect construction, execution, propagation, and typed-failure catching. */ export function* execute( context: OperationContext, operation: EffectOperation, @@ -609,7 +609,7 @@ export function* execute( }) return Object.freeze({ _tag: 'Value', value: propagated }) } - case 'ReifyEffect': { + case 'CatchEffect': { const effect = read(operation.effect).value if (effect._tag !== 'EffectValue') throw new RangeError('MIR attempted to reify a non-Effect value') diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index fa95c5787..a78f11aa8 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -374,7 +374,7 @@ function* executeFunction( arguments_: ReadonlyArray, operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' } >, ): FunctionExecution { const control = suspensionFor(operation) @@ -4475,7 +4475,7 @@ function* executeFunction( case 'RunEffectComposite': case 'RunEffectValue': case 'RunStaticEffect': - case 'ReifyEffect': { + case 'CatchEffect': { const effectStep = yield* BootstrapEffect.execute( { program, diff --git a/packages/compiler/src/EffectLowering.ts b/packages/compiler/src/EffectLowering.ts index 6012e418c..b896dbdb1 100644 --- a/packages/compiler/src/EffectLowering.ts +++ b/packages/compiler/src/EffectLowering.ts @@ -9,7 +9,7 @@ import { import * as ConformanceProof from './ConformanceProof.js' import type {} from './EntryAssembly.js' import type {} from './Forwarding.js' -import { effectRecipe, inlineForwardedRequirement } from './Forwarding.js' +import { inlineForwardedRequirement } from './Forwarding.js' import type { FunctionLowering } from './FunctionLowering.js' import * as Hir from './Hir.js' import * as Instances from './Instances.js' @@ -311,20 +311,20 @@ export const lowerRunEffectComposite = ( return Object.freeze({ result: destination }) } -export interface ReifiedEffect { +export interface CaughtEffect { readonly valid: Mir.LocalId readonly success: Mir.LocalId readonly failure: Mir.LocalId readonly failureValueType: Type.Type } -export const reifyEffectValue = ( +export const runCaughtEffectValue = ( fn: FunctionLowering, effect: Mir.LocalId, effectType: Extract, span: SourceSpan.SourceSpan, availableRequirements: ReadonlyArray = fn.providedRequirements, -): ReifiedEffect | undefined => { +): CaughtEffect | undefined => { const provided = requirementsFor(availableRequirements, effectType.type) if (provided === undefined) return undefined const runner = @@ -362,7 +362,7 @@ export const reifyEffectValue = ( const failure = fn.alloc(failureType) fn.emit( Object.freeze({ - _tag: 'ReifyEffect' as const, + _tag: 'CatchEffect' as const, destination: valid, outcome, successValue: success, @@ -390,7 +390,7 @@ export const reifyEffectValue = ( }) } -export const callableEffectResult = ( +export const callableEffectValue = ( fn: FunctionLowering, callable: Extract, ): Extract | undefined => { @@ -441,7 +441,7 @@ export const lowerEffectCatch = ( captured?.handlerType ?? (handler === undefined ? undefined : fn.localTypes.at(handler.result.ordinal)) const handlerEffectType = - handlerType?._tag === 'CallableValue' ? callableEffectResult(fn, handlerType) : undefined + handlerType?._tag === 'CallableValue' ? callableEffectValue(fn, handlerType) : undefined if ( handler === undefined || handlerType?._tag !== 'CallableValue' || @@ -528,12 +528,12 @@ export const lowerEffectCatch = ( ) return undefined - const reified = reifyEffectValue(fn, protected_.result, protectedType, expression.span) - if (reified === undefined) return undefined + const caught = runCaughtEffectValue(fn, protected_.result, protectedType, expression.span) + if (caught === undefined) return undefined const successType = fn.type(resultEffect.success) const successShape = Layout.callingShape(fn.layout, resultEffect.success) - const failureValueMir = fn.type(reified.failureValueType) + const failureValueMir = fn.type(caught.failureValueType) const propagationEffect = fn.effectOutcome const propagationType = propagationEffect === undefined ? undefined : fn.type(propagationEffect) const propagationShape = @@ -571,7 +571,7 @@ export const lowerEffectCatch = ( handlerType.typeArguments ?? Object.freeze([]), captures: Object.freeze([]), - arguments: Object.freeze([reified.failure]), + arguments: Object.freeze([caught.failure]), callableType: handlerType.type, access: handlerType.type.mode, evaluation: 'CalleeThenArguments' as const, @@ -588,8 +588,8 @@ export const lowerEffectCatch = ( Object.freeze({ _tag: 'Conditional' as const, destination, - condition: reified.valid, - taken: Object.freeze({ operations: unusedHandlerDrop(), result: reified.success }), + condition: caught.valid, + taken: Object.freeze({ operations: unusedHandlerDrop(), result: caught.success }), otherwise: Object.freeze({ operations: handledOperations, result: handled.result }), type: successType, resultShape: successShape, @@ -745,9 +745,9 @@ export const lowerEffectCatch = ( _tag: 'Match', id: innerMatch, destination: innerResult, - scrutinee: reified.failure, + scrutinee: caught.failure, scrutineeType: failureValueMir, - scrutineeShape: Layout.callingShape(fn.layout, reified.failureValueType) ?? successShape, + scrutineeShape: Layout.callingShape(fn.layout, caught.failureValueType) ?? successShape, access: 'Move', retainsBindings: false, members: failureCoverage, @@ -769,8 +769,8 @@ export const lowerEffectCatch = ( Object.freeze({ _tag: 'Conditional' as const, destination, - condition: reified.valid, - taken: Object.freeze({ operations: unusedHandlerDrop(), result: reified.success }), + condition: caught.valid, + taken: Object.freeze({ operations: unusedHandlerDrop(), result: caught.success }), otherwise: Object.freeze({ operations: Object.freeze([innerOperation]), result: innerResult, @@ -1046,13 +1046,13 @@ export const lowerServiceEffectValue = ( ) return undefined const typeArguments = call?.target.typeArguments ?? provided.witness.typeArguments - const effectResult = + const effectValue = (call?.resultEffect === undefined ? undefined : effectValueByIdentity(fn.layout, call.resultEffect)) ?? fn.effectResults.get(instanceText(target, typeArguments)) - if (effectResult === undefined) return undefined - const effect = fn.alloc(effectResult) + if (effectValue === undefined) return undefined + const effect = fn.alloc(effectValue) fn.emit( Object.freeze({ _tag: 'Call', @@ -1065,7 +1065,7 @@ export const lowerServiceEffectValue = ( argument === undefined ? [] : [argument.result], ), ]), - type: effectResult, + type: effectValue, provenance: authored(subject.span), }), ) @@ -1163,7 +1163,7 @@ const prepareProvidedEffect = ( } /** - * Brackets one provided requirement for reification or immediate execution. The actor that begins + * Brackets one provided requirement for catch handling or immediate execution. The actor that begins * a provider loan also ends it, removes its tracking entry, and drops a taken provider after the * protected lowering has finished. */ @@ -1246,143 +1246,6 @@ const lowerForwardedProvider = ( return result } -export const lowerReifiedEffectRecipe = ( - fn: FunctionLowering, - subject: Hir.Expression, - successCarrier: Hir.Expression, - failureCarrier: Hir.Expression, - resultType: Type.Type, - span: SourceSpan.SourceSpan, - availableRequirements: ReadonlyArray = fn.providedRequirements, -): LoweredExpression | undefined => { - const recipe = effectRecipe(fn, subject) - const forwarded = inlineForwardedRequirement(fn, recipe) - if (forwarded !== undefined) { - return lowerForwardedProvider(fn, forwarded, span, (requirement) => { - const reified = lowerReifiedEffectRecipe( - fn, - forwarded.binding.protected, - successCarrier, - failureCarrier, - resultType, - span, - Object.freeze([...availableRequirements, requirement]), - ) - if (reified === undefined) return undefined - endRunLoans(fn, span) - return reified - }) - } - - if (recipe._tag === 'EffectBindRequirement') { - return lowerProvidedEffect(fn, recipe.provider, (requirement) => { - const reified = lowerReifiedEffectRecipe( - fn, - recipe.protected, - successCarrier, - failureCarrier, - resultType, - span, - Object.freeze([...availableRequirements, requirement]), - ) - if (reified === undefined) return undefined - endRunLoans(fn, span) - return reified - }) - } - const lowered = - recipe._tag === 'ServiceEffectConstruct' - ? lowerServiceEffectValue(fn, recipe, availableRequirements) - : lowerExpression(fn, recipe) - const effectType = lowered === undefined ? undefined : fn.localTypes.at(lowered.result.ordinal) - if (lowered === undefined || effectType?._tag !== 'EffectValue') return undefined - const success = lowerExpression(fn, successCarrier) - const failure = lowerExpression(fn, failureCarrier) - const successType = success === undefined ? undefined : fn.localTypes.at(success.result.ordinal) - const failureType = failure === undefined ? undefined : fn.localTypes.at(failure.result.ordinal) - const carrierType = fn.type(resultType) - const carrierShape = - carrierType === undefined - ? undefined - : Layout.callingShape(fn.layout, Mir.semanticType(carrierType)) - if ( - success === undefined || - failure === undefined || - successType?._tag !== 'CallableValue' || - failureType?._tag !== 'CallableValue' || - carrierType === undefined || - carrierType._tag === 'EffectOutcome' || - carrierShape === undefined - ) - return undefined - const reified = reifyEffectValue(fn, lowered.result, effectType, span, availableRequirements) - if (reified === undefined) return undefined - const drop = ( - local: Mir.LocalId, - type: Extract, - ): ReadonlyArray => { - const cleanup = cleanupForLocal(fn, concreteCleanup(fn, Mir.semanticType(type)), type) - return cleanup._tag === 'NoCleanup' - ? Object.freeze([]) - : Object.freeze([ - Object.freeze({ - _tag: 'Drop' as const, - local, - cleanup, - provenance: generated(span), - }), - ]) - } - const apply = ( - callable: Mir.LocalId, - callableType: Extract, - argument: Mir.LocalId, - ): Extract => - Object.freeze({ - _tag: 'ApplyCallable', - destination: fn.alloc(carrierType), - callable, - typeArguments: - callableType.environment?.callable.typeArguments ?? - callableType.storage?.realization.targetArguments ?? - callableType.typeArguments ?? - Object.freeze([]), - captures: Object.freeze([]), - arguments: Object.freeze([argument]), - callableType: callableType.type, - access: callableType.type.mode, - evaluation: 'CalleeThenArguments', - realization: 'Environment', - type: carrierType, - provenance: generated(span), - }) - const successApply = apply(success.result, successType, reified.success) - const failureApply = apply(failure.result, failureType, reified.failure) - const destination = fn.alloc(carrierType) - fn.emit( - Object.freeze({ - _tag: 'Conditional' as const, - destination, - condition: reified.valid, - taken: Object.freeze({ - operations: Object.freeze([...drop(failure.result, failureType), successApply]), - result: successApply.destination, - }), - otherwise: Object.freeze({ - operations: Object.freeze([...drop(success.result, successType), failureApply]), - result: failureApply.destination, - }), - type: carrierType, - resultShape: carrierShape, - provenance: generated(span), - }), - ) - endRunLoans(fn, span) - if (recipe._tag === 'EffectConstruct' || recipe._tag === 'ServiceEffectConstruct') - endLoans(fn, recipe.loanEnds, span) - return Object.freeze({ result: destination }) -} - export const lowerEffectExecution = ( fn: FunctionLowering, subject: Hir.Expression, @@ -1465,13 +1328,12 @@ export const lowerEffectExecution = ( } if (subject._tag === 'ServiceEffectConstruct') { const lowered = lowerServiceEffectValue(fn, subject, availableRequirements) - const effectResult = - lowered === undefined ? undefined : fn.localTypes.at(lowered.result.ordinal) - if (lowered === undefined || effectResult?._tag !== 'EffectValue') return undefined + const effectValue = lowered === undefined ? undefined : fn.localTypes.at(lowered.result.ordinal) + if (lowered === undefined || effectValue?._tag !== 'EffectValue') return undefined const result = lowerRunEffectValue( fn, lowered.result, - effectResult, + effectValue, success, span, availableRequirements, diff --git a/packages/compiler/src/Elaboration.ts b/packages/compiler/src/Elaboration.ts index 0cfa5dda3..928337dd5 100644 --- a/packages/compiler/src/Elaboration.ts +++ b/packages/compiler/src/Elaboration.ts @@ -930,16 +930,6 @@ export type ExpressionFact = readonly type: ExpressionTypeFact readonly syntax: SyntaxTree.Node } - | { - /** Folds one typed Effect outcome through ordinary carrier functions without catching traps. */ - readonly _tag: 'EffectResult' - readonly reference: IntrinsicReferenceFact - readonly protected: ExpressionFact - readonly success: ExpressionFact - readonly failure: ExpressionFact - readonly type: ExpressionTypeFact - readonly syntax: SyntaxTree.Node - } | { /** `Place.replace(place, value)`: swap one writable place, yielding its old value. */ readonly _tag: 'PlaceReplace' diff --git a/packages/compiler/src/EntryAssembly.ts b/packages/compiler/src/EntryAssembly.ts index cecfb8215..b21d26aaf 100644 --- a/packages/compiler/src/EntryAssembly.ts +++ b/packages/compiler/src/EntryAssembly.ts @@ -201,7 +201,7 @@ export const lowerInstance = ( terminalStatement?._tag === 'Return' && 'type' in terminalStatement.expression ? terminalStatement.expression : undefined - const hiddenEffectResult = + const hiddenEffectValue = returnedBlock === undefined ? undefined : effectValueType(layout, instance.key, returnedBlock) const hiddenCompositeResult = returnedExpression === undefined @@ -212,13 +212,13 @@ export const lowerInstance = ( returnedExpression.type, instance.substitution, ) - const specializedEffectResult = + const specializedEffectValue = instance.resultEffect === undefined ? undefined : effectValueByIdentity(layout, instance.resultEffect) const resultType = - specializedEffectResult ?? - hiddenEffectResult ?? + specializedEffectValue ?? + hiddenEffectValue ?? hiddenCompositeResult ?? (contract._tag === 'Contract' ? (storedCallableValueType(layout, effectOutcome ?? instance.specialization.result) ?? diff --git a/packages/compiler/src/ExecutableOrigin.ts b/packages/compiler/src/ExecutableOrigin.ts index ab41bc569..e9ce74272 100644 --- a/packages/compiler/src/ExecutableOrigin.ts +++ b/packages/compiler/src/ExecutableOrigin.ts @@ -289,12 +289,6 @@ export const make = (operations: Operations) => { substitution: Type.Substitution, ): ReadonlyArray => { if (expression._tag === 'Run') return callTargets(expression.subject, index, substitution) - if (expression._tag === 'EffectResult') - return [ - ...callTargets(expression.protected, index, substitution), - ...callTargets(expression.success, index, substitution), - ...callTargets(expression.failure, index, substitution), - ] if (expression._tag === 'EffectCatch') return [ ...callTargets(expression.protected, index, substitution), @@ -886,15 +880,7 @@ export const make = (operations: Operations) => { ? undefined : resultEffectIdentity(serviceFunction, serviceTarget, context.results, context.index) } - if (identity === undefined) { - if ( - forwardedEffectResultParameter(target) === ordinal && - argument !== undefined && - requirementBoundEffectRecipe(argument, context) - ) - continue - return undefined - } + if (identity === undefined) return undefined hiddenArguments.push(Type.effectIdentityArgument(identity)) } for (const ordinal of callableParameterOrdinals(target, targetSubstitution)) { @@ -959,51 +945,6 @@ export const make = (operations: Operations) => { }) } - const forwardedEffectResultParameter = (target: Hir.HirFunction): number | undefined => { - const returned = target.statements.at(-1) - if (target.statements.length !== 1 || returned?._tag !== 'Return') return undefined - const block = returned.expression - const completed = block._tag === 'EffectBlock' ? block.statements.at(-1) : undefined - const run = completed?._tag === 'Return' ? completed.expression : undefined - const result = run?._tag === 'Run' ? run.subject : undefined - if ( - block._tag !== 'EffectBlock' || - block.statements.length !== 1 || - result?._tag !== 'EffectResult' - ) - return undefined - const parameterOrdinal = (expression: Hir.Expression): number | undefined => { - const parameter = expression._tag === 'Move' ? expression.subject : expression - return parameter._tag === 'ParameterReference' ? parameter.parameter.ordinal : undefined - } - const protected_ = parameterOrdinal(result.protected) - return protected_ - } - - const requirementBoundEffectRecipe = ( - expression: Hir.Expression, - context: EffectOriginContext, - resolving: ReadonlySet = new Set(), - ): boolean => { - if (expression._tag === 'Move') - return requirementBoundEffectRecipe(expression.subject, context, resolving) - if (expression._tag === 'UnionConvert') - return requirementBoundEffectRecipe(expression.source, context, resolving) - if (expression._tag === 'BindingReference') { - const ordinal = expression.binding.ordinal - if (resolving.has(ordinal)) return false - const initializer = callableBindings(context.fn).get(ordinal) - return ( - initializer !== undefined && - requirementBoundEffectRecipe(initializer, context, new Set(resolving).add(ordinal)) - ) - } - if (expression._tag === 'EffectBindRequirement') return true - if (expression._tag !== 'Call' && expression._tag !== 'EffectConstruct') return false - const target = targetFunction(context.results, expression.target) - return target !== undefined && forwardedRequirementBinding(target) !== undefined - } - function serviceEffectRecipes( expression: Hir.Expression, context: EffectOriginContext, @@ -1021,8 +962,6 @@ export const make = (operations: Operations) => { ) if (expression._tag === 'Run') return serviceEffectRecipes(expression.subject, context, resolving) - if (expression._tag === 'EffectResult') - return serviceEffectRecipes(expression.protected, context, resolving) if (expression._tag === 'Move') return serviceEffectRecipes(expression.subject, context, resolving) if (expression._tag === 'UnionConvert') @@ -1313,15 +1252,7 @@ export const make = (operations: Operations) => { ? undefined : resultEffectIdentity(serviceFunction, serviceTarget, context.results, context.index) } - if (identity === undefined) { - if ( - forwardedEffectResultParameter(target) === ordinal && - argument !== undefined && - requirementBoundEffectRecipe(argument, context) - ) - continue - return undefined - } + if (identity === undefined) return undefined hiddenArguments.push(Type.effectIdentityArgument(identity)) } for (const ordinal of callableParameterOrdinals(target, targetSubstitution)) { @@ -2466,7 +2397,6 @@ export const make = (operations: Operations) => { const executionTargets = (expression: Hir.Expression): ReadonlyArray => { if (expression._tag === 'EffectBindRequirement') return Object.freeze([providerBindingNode(instance.key, expression)]) - if (expression._tag === 'EffectResult') return executionTargets(expression.protected) if (expression._tag === 'BindingReference') { const initializer = bindings.get(expression.binding.ordinal) return initializer === undefined ? [] : executionTargets(initializer) diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index 8051516e6..121ea2472 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -5028,191 +5028,6 @@ export const intrinsicReference = ( }) } -export const isEffectResultTarget = ( - source: SourceFile.SourceFile, - node: SyntaxTree.Node, -): boolean => { - const rule = intrinsicOperationTarget(source, node)?.rule - return rule?._tag === 'EffectRule' && rule.operation === 'Result' -} - -export const analyzeEffectResult = ( - source: SourceFile.SourceFile, - node: SyntaxTree.Node, - declarations: ReadonlyArray, - declaration: DeclarationFact, - scope: Scope, - resolution: ResolutionContext, - expected?: SemanticType, -): ExpressionResult => { - const pipelined = node.kind === 'PipelineExpression' - const target = pipelined ? (pipelineCallable(node) ?? node) : node - const list = SyntaxTree.directNode(target, 'ArgumentList') - const argumentNodes = - list?.children.filter((element): element is SyntaxTree.Node => - isRecursiveArgumentNode(element), - ) ?? [] - const protectedNode = pipelined ? pipelineInput(node) : argumentNodes.at(0) - const successNode = argumentNodes.at(pipelined ? 0 : 1) - const failureNode = argumentNodes.at(pipelined ? 1 : 2) - const callTypeArguments = analyzeCallTypeArguments(source, target, declaration, resolution) - const protectedResult = - protectedNode === undefined - ? undefined - : analyzeExpression(source, protectedNode, declarations, declaration, scope, resolution) - const protectedEffect = - protectedResult?.type !== undefined && Type.isEffect(protectedResult.type) - ? protectedResult.type - : undefined - const expectedEffect = expected !== undefined && Type.isEffect(expected) ? expected : undefined - let carrierResult = expectedEffect?.success ?? callTypeArguments.types?.at(0) - let successResult = - successNode === undefined - ? undefined - : analyzeExpression( - source, - successNode, - declarations, - declaration, - scope, - resolution, - protectedEffect === undefined || carrierResult === undefined - ? undefined - : Type.callable(Object.freeze([protectedEffect.success]), carrierResult, 'Take'), - ) - let failureResult = - failureNode === undefined - ? undefined - : analyzeExpression( - source, - failureNode, - declarations, - declaration, - scope, - resolution, - protectedEffect === undefined || carrierResult === undefined - ? undefined - : Type.callable( - Object.freeze([Type.failureType(protectedEffect)]), - carrierResult, - 'Take', - ), - ) - const successCallable = - successResult?.type !== undefined && Type.isCallable(successResult.type) - ? successResult.type - : undefined - const failureCallable = - failureResult?.type !== undefined && Type.isCallable(failureResult.type) - ? failureResult.type - : undefined - carrierResult ??= successCallable?.result ?? failureCallable?.result - if (protectedEffect !== undefined && carrierResult !== undefined) { - if (successNode !== undefined) - successResult = analyzeExpression( - source, - successNode, - declarations, - declaration, - scope, - resolution, - Type.callable(Object.freeze([protectedEffect.success]), carrierResult, 'Take'), - ) - if (failureNode !== undefined) - failureResult = analyzeExpression( - source, - failureNode, - declarations, - declaration, - scope, - resolution, - Type.callable(Object.freeze([Type.failureType(protectedEffect)]), carrierResult, 'Take'), - ) - } - const diagnostics: Array = [ - ...callTypeArguments.diagnostics, - ...(protectedResult?.diagnostics ?? []), - ...(successResult?.diagnostics ?? []), - ...(failureResult?.diagnostics ?? []), - ] - if (argumentNodes.length !== (pipelined ? 2 : 3)) - diagnostics.push( - Diagnostic.invalidEffectHandler( - 'result requires one Effect plus success and failure carriers', - node.span, - ), - ) - if (protectedEffect === undefined) - diagnostics.push( - Diagnostic.invalidEffectHandler( - 'the protected argument is not an Effect', - protectedNode?.span ?? node.span, - ), - ) - const expectedSuccess = - protectedEffect === undefined || carrierResult === undefined - ? undefined - : Type.callable(Object.freeze([protectedEffect.success]), carrierResult, 'Take') - const expectedFailure = - protectedEffect === undefined || carrierResult === undefined - ? undefined - : Type.callable(Object.freeze([Type.failureType(protectedEffect)]), carrierResult, 'Take') - if ( - expectedSuccess !== undefined && - (successResult?.type === undefined || !typesCompatible(successResult.type, expectedSuccess)) - ) - diagnostics.push( - Diagnostic.invalidEffectHandler( - 'the success carrier must accept the Effect success and return the shared result type', - successNode?.span ?? node.span, - ), - ) - if ( - expectedFailure !== undefined && - (failureResult?.type === undefined || !typesCompatible(failureResult.type, expectedFailure)) - ) - diagnostics.push( - Diagnostic.invalidEffectHandler( - 'the failure carrier must accept the Effect failure and return the shared result type', - failureNode?.span ?? node.span, - ), - ) - const type = - protectedEffect === undefined || carrierResult === undefined - ? unavailableExpressionType - : availableExpressionType( - Type.effectWithRows( - carrierResult, - RowAlgebra.concrete(Type.failureRowPolicy(), []), - strongestEffectAccess( - protectedResult === undefined - ? protectedEffect.access - : effectExpressionAccess(protectedResult.fact, resolution.index), - ...(successResult === undefined - ? [] - : [effectExpressionAccess(successResult.fact, resolution.index)]), - ...(failureResult === undefined - ? [] - : [effectExpressionAccess(failureResult.fact, resolution.index)]), - ), - protectedEffect.requirementRow, - ), - ) - return Object.freeze({ - fact: Object.freeze({ - _tag: 'EffectResult', - reference: intrinsicReference(source, target), - protected: protectedResult?.fact ?? unavailableExpression(node), - success: successResult?.fact ?? unavailableExpression(successNode ?? node), - failure: failureResult?.fact ?? unavailableExpression(failureNode ?? node), - type, - syntax: node, - }), - diagnostics: Object.freeze(diagnostics), - type: type._tag === 'Available' ? type.type : undefined, - }) -} - /** Finalizes ordinary lexical captures for one source Effect body. */ export const effectCaptureFacts = ( statements: ReadonlyArray, @@ -5325,9 +5140,6 @@ export const effectCaptureFacts = ( case 'Run': expression(fact.subject) return - case 'EffectResult': - expression(fact.protected) - return case 'EffectCatch': expression(fact.protected) expression(fact.handler) @@ -5956,17 +5768,6 @@ export function analyzeExpression( } if (node.kind === 'PipelineExpression' || node.kind === 'CallExpression') { - const operationTarget = node.kind === 'PipelineExpression' ? pipelineCallable(node) : node - if (operationTarget !== undefined && isEffectResultTarget(source, operationTarget)) - return analyzeEffectResult( - source, - node, - declarations, - declaration, - scope, - resolution, - expected, - ) if (node.kind === 'PipelineExpression') return analyzePipelineExpression(source, node, declarations, declaration, scope, resolution) } diff --git a/packages/compiler/src/Hir.ts b/packages/compiler/src/Hir.ts index a9138e407..b86879b4b 100644 --- a/packages/compiler/src/Hir.ts +++ b/packages/compiler/src/Hir.ts @@ -732,14 +732,6 @@ export type Expression = readonly type: DeclarationFacts.SemanticType readonly span: SourceSpan.SourceSpan } - | { - readonly _tag: 'EffectResult' - readonly protected: Expression - readonly success: Expression - readonly failure: Expression - readonly type: Type.Effect - readonly span: SourceSpan.SourceSpan - } | { /** * Member-selective recovery, carrying the four rows the semantic-fact surface records. @@ -1047,8 +1039,6 @@ export const expressionChildren = (expression: Expression): ReadonlyArray { `${indent}run : ${Type.encode(expression.type)} ${spanText(expression.span)}`, encodeExpression(expression.subject, depth + 1), ].join('\n') - case 'EffectResult': - return [ - `${indent}effect-result : ${Type.encode(expression.type)} ${spanText(expression.span)}`, - encodeExpression(expression.protected, depth + 1), - encodeExpression(expression.success, depth + 1), - encodeExpression(expression.failure, depth + 1), - ].join('\n') case 'EffectCatch': return [ `${indent}effect-catch intrinsic=${Intrinsic.operationText(expression.intrinsic)} ${Type.encode(expression.selected)} protected=${RowAlgebra.encode( diff --git a/packages/compiler/src/HirLowering.ts b/packages/compiler/src/HirLowering.ts index d9818cd16..0171e1682 100644 --- a/packages/compiler/src/HirLowering.ts +++ b/packages/compiler/src/HirLowering.ts @@ -573,27 +573,6 @@ export const hirExpression = (fact: ExpressionFact, borrow?: Hir.BorrowId): Hir. span: fact.syntax.span, }) } - if (fact._tag === 'EffectResult') { - const protected_ = hirExpression(fact.protected) - const success = hirExpression(fact.success) - const failure = hirExpression(fact.failure) - if ( - protected_._tag === 'Unavailable' || - success._tag === 'Unavailable' || - failure._tag === 'Unavailable' || - fact.type._tag !== 'Available' || - !Type.isEffect(fact.type.type) - ) - return Object.freeze({ _tag: 'Unavailable', span: fact.syntax.span }) - return Object.freeze({ - _tag: 'EffectResult', - protected: protected_, - success, - failure, - type: fact.type.type, - span: fact.syntax.span, - }) - } if (fact._tag === 'EffectCatch') { const protected_ = hirExpression(fact.protected) const handler = hirExpression(fact.handler) @@ -1651,8 +1630,6 @@ export const directExpressionChildren = ( return Object.freeze(expression.initializers.map((initializer) => initializer.expression)) case 'Grouped': return Object.freeze([expression.expression]) - case 'EffectResult': - return Object.freeze([expression.protected]) case 'EffectBindRequirement': return Object.freeze([expression.protected]) case 'EffectCatch': diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index 3c0ac4e5b..9a37f269b 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -857,7 +857,7 @@ const operationLabel = (operation: Mir.Operation): string => { return `${localText(operation.destination)} = run effect choice ${localText(operation.effect)}` case 'RunStaticEffect': return `${localText(operation.destination)} = run static ${operation.runner.name} with ${operation.captures.map((capture) => localText(capture.source)).join(', ') || 'no captures'}` - case 'ReifyEffect': + case 'CatchEffect': return `${localText(operation.destination)} = result ${localText(operation.effect)} with ${operation.runner.name}` case 'CloseEffectEntry': return `${localText(operation.destination)} = close ${operation.target.name} with ${operation.runner.name}` diff --git a/packages/compiler/src/InspectorProjectSyntax.ts b/packages/compiler/src/InspectorProjectSyntax.ts index 03eaea5bc..1a6b56bc3 100644 --- a/packages/compiler/src/InspectorProjectSyntax.ts +++ b/packages/compiler/src/InspectorProjectSyntax.ts @@ -269,8 +269,6 @@ const hirExpressionLabel = (expression: Hir.Expression): string => { return `effect block · ${expression.type.access.toLowerCase()}` case 'Run': return 'run recipe' - case 'EffectResult': - return 'materialize effect result' case 'EffectBindRequirement': return expression.provider.capability === undefined ? 'bind selected requirement' @@ -315,11 +313,6 @@ export const hirRows = (hir: Hir.Module): ReadonlyArray => { expression(argument, depth + 1, `${path}.argument${index}`) }) } - if (node._tag === 'EffectResult') { - expression(node.protected, depth + 1, `${path}.protected`) - expression(node.success, depth + 1, `${path}.success`) - expression(node.failure, depth + 1, `${path}.failure`) - } if (node._tag === 'EffectBindRequirement') { expression(node.protected, depth + 1, `${path}.protected`) } diff --git a/packages/compiler/src/Intrinsic.ts b/packages/compiler/src/Intrinsic.ts index a045fbaa1..eba887754 100644 --- a/packages/compiler/src/Intrinsic.ts +++ b/packages/compiler/src/Intrinsic.ts @@ -64,10 +64,6 @@ export type Rule = readonly parameters: ReadonlyArray readonly result: Type.Type } - | { - readonly _tag: 'EffectRule' - readonly operation: 'Result' - } | { readonly _tag: 'ContractRule' readonly contract: CallableContract.CallableContract @@ -260,32 +256,6 @@ const builtin = (options: { export const isBuiltinOperation = (operation: Operation): operation is BuiltinOperation => operation.rule._tag === 'BuiltinRule' && 'callParameters' in operation -const effect = (options: { - readonly name: string - readonly operation: Extract['operation'] - readonly typeParameters: ReadonlyArray - readonly parameters: ReadonlyArray - readonly result: string -}): Operation => { - const spelling = intrinsicSpelling('Effect', options.name) - return Object.freeze({ - _tag: 'IntrinsicOperation', - id: operationId('Intrinsic', spelling), - spelling, - typeParameters: Object.freeze(options.typeParameters.map(typeParameter)), - parameters: Object.freeze(Array.from(options.parameters)), - result: options.result, - unsafe: false, - admission: admission('Effect'), - consumer: consumer('Effect', options.name), - targets: executionTargets, - rule: Object.freeze({ - _tag: 'EffectRule', - operation: options.operation, - }), - }) -} - const contractEffect = (options: { readonly name: string readonly typeParameters: ReadonlyArray @@ -1639,17 +1609,6 @@ const intrinsicOperations = Object.freeze([ suspensionRequirementRow, ), }), - effect({ - name: 'result', - operation: 'Result', - typeParameters: Object.freeze(['R']), - parameters: Object.freeze([ - valueParameter('protected', 'Effect'), - valueParameter('success', 'once fn(A) -> R'), - valueParameter('failure', 'once fn(E) -> R'), - ]), - result: 'Effect', - }), contractEffect({ name: 'bindRequirement', post: 'BindRequirement', @@ -1788,9 +1747,6 @@ export const inventory = (): ReadonlyArray => case 'BuiltinRule': identity = operation.rule.operation break - case 'EffectRule': - identity = `${operation.rule._tag}.${operation.rule.operation}` - break case 'ContractRule': identity = `${operation.rule._tag}.${operation.rule.post}` break diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 1d0855a9b..e045bfed5 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -1872,10 +1872,11 @@ export const catalog = ( } if (child._tag === 'EffectCatch' && child.protected._tag !== 'Unavailable') { const protected_ = Type.substitute(child.protected.type, substitution) - if (Type.isEffect(protected_)) - addReferenced( - Type.result(protected_.success, Type.failureValue(Type.failureMembers(protected_))), - ) + if (Type.isEffect(protected_)) { + addReferenced('bool') + addReferenced(protected_.success) + addReferenced(Type.failureValue(Type.failureMembers(protected_))) + } } } } @@ -2047,16 +2048,15 @@ const addExpressionTypes = ( } if (expression._tag === 'EffectCatch') { types.set(Type.key('never'), 'never') + types.set(Type.key('bool'), 'bool') addExpressionTypes(types, expression.protected, substitution) addExpressionTypes(types, expression.handler, substitution) if (expression.protected._tag !== 'Unavailable') { const protected_ = Type.substitute(expression.protected.type, substitution) if (Type.isEffect(protected_)) { - const reified = Type.result( - protected_.success, - Type.failureValue(Type.failureMembers(protected_)), - ) - types.set(Type.key(reified), reified) + types.set(Type.key(protected_.success), protected_.success) + const failure = Type.failureValue(Type.failureMembers(protected_)) + types.set(Type.key(failure), failure) } } } @@ -2839,11 +2839,6 @@ export const plan = ( .flatMap(Hir.statementExpressions) .flatMap(Hir.expressionTree)) { if (expression._tag === 'EffectCatch') reached.set(Type.key('bool'), 'bool') - if (expression._tag === 'EffectResult') { - reached.set(Type.key('bool'), 'bool') - const result = Type.substitute(expression.type, instance.substitution) - reached.set(Type.key(result), result) - } if ( expression._tag !== 'BuiltinCall' || (expression.operation !== 'ExecutionLayout' && diff --git a/packages/compiler/src/LocalSharedAllocationProvenance.ts b/packages/compiler/src/LocalSharedAllocationProvenance.ts index 97893a651..568aefe9b 100644 --- a/packages/compiler/src/LocalSharedAllocationProvenance.ts +++ b/packages/compiler/src/LocalSharedAllocationProvenance.ts @@ -427,8 +427,6 @@ export const plan = (discovery: Instances.Discovery, index: DeclarationIndex.Ind provider: expression.provider, span: expression.span, }) - if (expression._tag === 'EffectResult') - return originOf(expression.protected, instance, parameterOrigins, resolving, activeBindings) if (expression._tag === 'EffectCatch') return originOf(expression.protected, instance, parameterOrigins, resolving, activeBindings) if (expression._tag === 'EffectBlock') { diff --git a/packages/compiler/src/Lower.ts b/packages/compiler/src/Lower.ts index 0131884b6..04f74bb6c 100644 --- a/packages/compiler/src/Lower.ts +++ b/packages/compiler/src/Lower.ts @@ -438,7 +438,7 @@ export const lowerProgram = ( if ( operation._tag !== 'RunEffectValue' && operation._tag !== 'RunStaticEffect' && - operation._tag !== 'ReifyEffect' + operation._tag !== 'CatchEffect' ) continue const key = runnerKey(operation.runner, operation.runnerTypeArguments) diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 88fc298c9..889d2388f 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -23,7 +23,6 @@ import { lowerEffectCatch, lowerEffectExecution, lowerPlace, - lowerReifiedEffectRecipe, lowerRunEffectComposite, ownedWriteRoot, } from './EffectLowering.js' @@ -540,7 +539,7 @@ export function lowerExpressionInner( const definition = callable === undefined ? undefined : fn.callableDefinitions.get(callable.ordinal) const realizedTarget = target ?? definition?.target - const declaredEffectResult = + const declaredEffectValue = realizedTarget?._tag === 'DeclarationCallableTarget' ? fn.effectResults.get(instanceText(realizedTarget.declaration, typeArguments)) : undefined @@ -548,7 +547,7 @@ export function lowerExpressionInner( (call?.resultEffect === undefined ? undefined : effectValueByIdentity(fn.layout, call.resultEffect)) ?? - declaredEffectResult ?? + declaredEffectValue ?? fn.type(expression.type) if (!lowered || type === undefined || callableType === undefined) return undefined if ( @@ -761,24 +760,11 @@ export function lowerExpressionInner( } case 'EffectCatch': return lowerCatchEffectValue(fn, expression) - case 'EffectResult': - return undefined case 'Run': { return fn.withRecipeReplay(() => { const resultRecipe = effectRecipe(fn, expression.subject) if (resultRecipe?._tag === 'EffectCatch') return lowerEffectCatch(fn, resultRecipe, expression.span) - if (resultRecipe?._tag === 'EffectResult') { - const reified = lowerReifiedEffectRecipe( - fn, - resultRecipe.protected, - resultRecipe.success, - resultRecipe.failure, - expression.type, - expression.span, - ) - return reified - } if ( resultRecipe !== undefined && inlineForwardedRequirement(fn, resultRecipe) !== undefined diff --git a/packages/compiler/src/LowerStatements.ts b/packages/compiler/src/LowerStatements.ts index cdb973dbc..5ab575ff0 100644 --- a/packages/compiler/src/LowerStatements.ts +++ b/packages/compiler/src/LowerStatements.ts @@ -365,7 +365,6 @@ export const lowerSequence = ( statement.initializer.typeArguments.map((argument) => fn.semanticArgument(argument)), ), ) === undefined) || - statement.initializer._tag === 'EffectResult' || statement.initializer._tag === 'EffectBindRequirement' || (statement.initializer._tag === 'Match' && effectContract(initializerType ?? 'never') !== undefined) || diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index 5285b816d..66457c557 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -975,8 +975,8 @@ export type Operation = readonly provenance: Provenance } | { - /** Runs one Effect and exposes its completed typed channel without choosing a carrier. */ - readonly _tag: 'ReifyEffect' + /** Runs the protected Effect and exposes its channels only to the enclosing catch lowering. */ + readonly _tag: 'CatchEffect' readonly destination: LocalId readonly outcome: LocalId readonly successValue: LocalId diff --git a/packages/compiler/src/MirEncoding.ts b/packages/compiler/src/MirEncoding.ts index fc4a56086..f1cce3cdf 100644 --- a/packages/compiler/src/MirEncoding.ts +++ b/packages/compiler/src/MirEncoding.ts @@ -175,8 +175,8 @@ const operationText = (operation: Operation): string => { return `${localText(operation.destination)} = run-effect-composite ${localText(operation.effect)} alternatives=${operation.alternatives.map((alternative) => targetText(alternative.runner)).join(',')} arguments=${operation.arguments.map(localText).join(',') || 'none'} propagate=${operation.tagMappings.map((mapping) => `${mapping.source}->${mapping.target}`).join(',')} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` case 'RunStaticEffect': return `${localText(operation.destination)} = run-static-effect runner=${targetText(operation.runner)} captures=${operation.captures.map((capture) => `${localText(capture.source)}:${capture.access.toLowerCase()}`).join(',') || 'none'} arguments=${operation.arguments.map(localText).join(',') || 'none'} propagate=${operation.tagMappings.map((mapping) => `${mapping.source}->${mapping.target}`).join(',')} : ${typeText(operation.type)} ${operation.failureLoanEnds === undefined || operation.failureLoanEnds.length === 0 ? '' : `failure-loans=${operation.failureLoanEnds.map((ending) => `l${ending.borrow.ordinal}:${localText(ending.slice)}`).join(',')} `}${operation.releases === undefined || operation.releases.length === 0 ? '' : `releases=${operation.releases.map((release) => localText(release.local)).join(',')} `}${provenanceText(operation.provenance)}` - case 'ReifyEffect': - return `${localText(operation.destination)} = effect-result ${localText(operation.effect)} runner=${targetText(operation.runner)} arguments=${operation.arguments.map(localText).join(',') || 'none'} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` + case 'CatchEffect': + return `${localText(operation.destination)} = catch-effect ${localText(operation.effect)} runner=${targetText(operation.runner)} arguments=${operation.arguments.map(localText).join(',') || 'none'} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` case 'CloseEffectEntry': return `${localText(operation.destination)} = close-effect-entry ${targetText(operation.target)} effect=${localText(operation.effect)} runner=${targetText(operation.runner)} outcome=${localText(operation.outcome)} failures=${operation.failures.map((failure) => `${failure.tag}:${SilkType.encode(failure.type)}->${localText(failure.payload)}:${failure.cleanup._tag}`).join(',') || 'none'} : i32 ${provenanceText(operation.provenance)}` case 'Construct': diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index d77eb5874..0127bd73f 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -127,7 +127,7 @@ export const destinationOf = (operation: LinearOperation): Mir.LocalId | undefin case 'RunEffect': case 'RunEffectValue': case 'RunStaticEffect': - case 'ReifyEffect': + case 'CatchEffect': case 'CloseEffectEntry': case 'Construct': case 'ConstructUnionVariant': @@ -193,7 +193,7 @@ export const opensRuntimeContinuation = (operation: LinearOperation): boolean => operation._tag === 'RunEffectValue' || operation._tag === 'RunEffectComposite' || operation._tag === 'RunStaticEffect' || - operation._tag === 'ReifyEffect' || + operation._tag === 'CatchEffect' || operation._tag === 'CloseEffectEntry' || (operation._tag === 'Binary' && operation.operator !== 'Equals' && diff --git a/packages/compiler/src/MirNormalization.ts b/packages/compiler/src/MirNormalization.ts index 1a6c4d0cb..9b76f1ad4 100644 --- a/packages/compiler/src/MirNormalization.ts +++ b/packages/compiler/src/MirNormalization.ts @@ -190,7 +190,7 @@ const operationClassification = ( fn: Mir.MirFunction, operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'CloseEffectEntry' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'CloseEffectEntry' } >, ): ProvisionalMir.Classification => operation._tag === 'CloseEffectEntry' @@ -272,7 +272,7 @@ export const normalize = (program: Mir.Module, provisional: ProvisionalMir.Modul for (const operation of region.operations) { if ( operation._tag !== 'RunEffect' && - operation._tag !== 'ReifyEffect' && + operation._tag !== 'CatchEffect' && operation._tag !== 'CloseEffectEntry' ) continue diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 2b56f9285..b03891bef 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -501,7 +501,7 @@ const suspensionViolations = (fn: MirFunction, layout: Layout.Plan): ReadonlyArr invalid('InvalidSuspension', 'suspension runner identity disagrees with its exact MIR call') if ( (region.completion._tag === 'Propagate' && - (effectOperation._tag === 'ReifyEffect' || + (effectOperation._tag === 'CatchEffect' || region.completion.failureMappings.length !== SilkType.failureMembers(effectOperation.outcomeType.type).length || region.completion.failureMappings.some((mapping, ordinal) => { @@ -526,7 +526,7 @@ const suspensionViolations = (fn: MirFunction, layout: Layout.Plan): ReadonlyArr ) }))) || (region.completion._tag === 'Reify' && - (effectOperation._tag !== 'ReifyEffect' || + (effectOperation._tag !== 'CatchEffect' || !SilkType.equals( region.completion.successType, effectOperation.outcomeType.type.success, @@ -813,7 +813,7 @@ export const operationLocals = (operation: Operation): ReadonlyArray => ...operation.captures.map((capture) => capture.source), ...operation.arguments, ] - case 'ReifyEffect': + case 'CatchEffect': return [ operation.destination, operation.outcome, @@ -1689,7 +1689,7 @@ const operationTypes = (operation: Operation): ReadonlyArray => { ] case 'RunStaticEffect': return [...operation.captures.map((capture) => capture.source), ...operation.arguments] - case 'ReifyEffect': + case 'CatchEffect': return [operation.effect, ...operation.arguments] case 'CloseEffectEntry': return [] @@ -2088,7 +2088,7 @@ const suspensionCallTargets = (operation: Operation): ReadonlyArray => { }), ) } - if (operation._tag === 'ReifyEffect') { + if (operation._tag === 'CatchEffect') { const runner = self.functions.find((candidate) => matchesInstance(candidate, operation.runner, operation.runnerTypeArguments), ) diff --git a/packages/compiler/src/NativeCall.ts b/packages/compiler/src/NativeCall.ts index e0829f01a..ba6655dd5 100644 --- a/packages/compiler/src/NativeCall.ts +++ b/packages/compiler/src/NativeCall.ts @@ -51,7 +51,7 @@ export const callSynchronous = Effect.fnUntraced(function* ( export const operationInputs = ( operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'ExecutionPark' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'ExecutionPark' } >, ): ReadonlyArray => { if (operation._tag === 'ExecutionPark') { diff --git a/packages/compiler/src/NativeEffectOperation.ts b/packages/compiler/src/NativeEffectOperation.ts index a15defb23..0dbd5b03c 100644 --- a/packages/compiler/src/NativeEffectOperation.ts +++ b/packages/compiler/src/NativeEffectOperation.ts @@ -31,7 +31,7 @@ type Operation = Extract< | 'RunEffectComposite' | 'RunEffectValue' | 'RunStaticEffect' - | 'ReifyEffect' + | 'CatchEffect' | 'CloseEffectEntry' } > @@ -796,7 +796,7 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op nativeStorage.locals.set(operation.destination.ordinal, Object.freeze(loaded)) break } - case 'ReifyEffect': { + case 'CatchEffect': { const target = declared.find((candidate) => Mir.matchesInstance(candidate.fn, operation.runner, operation.runnerTypeArguments), ) diff --git a/packages/compiler/src/NativeOperation.ts b/packages/compiler/src/NativeOperation.ts index ca739cbb2..afd906c30 100644 --- a/packages/compiler/src/NativeOperation.ts +++ b/packages/compiler/src/NativeOperation.ts @@ -125,7 +125,7 @@ export const emit = Effect.fnUntraced(function* ( case 'RunEffectComposite': case 'RunEffectValue': case 'RunStaticEffect': - case 'ReifyEffect': + case 'CatchEffect': case 'CloseEffectEntry': return yield* NativeEffectOperation.emit(context.effect, operation) case 'ApplyCallable': diff --git a/packages/compiler/src/NativeSuspension.ts b/packages/compiler/src/NativeSuspension.ts index 482f44812..0f8c1cdb0 100644 --- a/packages/compiler/src/NativeSuspension.ts +++ b/packages/compiler/src/NativeSuspension.ts @@ -708,7 +708,7 @@ export const emitOrigin = Effect.fnUntraced(function* ( context: OperationContext, operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' } >, arguments_: ReadonlyArray, name: string, @@ -740,7 +740,7 @@ export const joinOutcome = Effect.fnUntraced(function* ( context: OperationContext, operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' } >, completedValues: ReadonlyArray, name: string, diff --git a/packages/compiler/src/Ownership.ts b/packages/compiler/src/Ownership.ts index 9fa7c49d2..ae88dd354 100644 --- a/packages/compiler/src/Ownership.ts +++ b/packages/compiler/src/Ownership.ts @@ -1013,33 +1013,6 @@ const checkExpression = ( ) return } - case 'EffectResult': - // The intrinsic consumes all three operands exactly like its source-callable contract. - checkExpression( - state, - live, - expression.protected, - argumentConsumes(expression.protected), - guard, - escaping, - ) - checkExpression( - state, - live, - expression.success, - argumentConsumes(expression.success), - guard, - escaping, - ) - checkExpression( - state, - live, - expression.failure, - argumentConsumes(expression.failure), - guard, - escaping, - ) - return case 'EffectCatch': // The sealed primitive has the same owned operands as its ordinary callable contract. // Visiting both here preserves take-once use checking after elaboration replaces the call @@ -1416,12 +1389,6 @@ const analyzeLoans = ( return Object.freeze( expression.elements.flatMap((element) => movedExecutableBindings(element.expression)), ) - if (expression._tag === 'EffectResult') - return Object.freeze([ - ...movedExecutableBindings(expression.protected), - ...movedExecutableBindings(expression.success), - ...movedExecutableBindings(expression.failure), - ]) if (expression._tag === 'EffectCatch') return Object.freeze([ ...movedExecutableBindings(expression.protected), @@ -1549,11 +1516,6 @@ const analyzeLoans = ( scanRunEnds(expression.protected, region) scanRunEnds(expression.handler, region) return - case 'EffectResult': - scanRunEnds(expression.protected, region) - scanRunEnds(expression.success, region) - scanRunEnds(expression.failure, region) - return case 'EffectBindRequirement': scanRunEnds(expression.protected, region) return diff --git a/packages/compiler/src/ProvisionalMir.ts b/packages/compiler/src/ProvisionalMir.ts index 9d5f323ff..260787074 100644 --- a/packages/compiler/src/ProvisionalMir.ts +++ b/packages/compiler/src/ProvisionalMir.ts @@ -346,8 +346,6 @@ const storedEffectRealizationOf = ( return storedEffectRealizationOf(expression.source, context) if (expression._tag === 'EffectBindRequirement') return storedEffectRealizationOf(expression.protected, context) - if (expression._tag === 'EffectResult') - return storedEffectRealizationOf(expression.protected, context) if (expression._tag !== 'Project') return undefined const represented = Type.substitute(expression.type, context.instance.substitution) const planned = Layout.entry(context.layout, represented)?.representation @@ -400,7 +398,6 @@ const effectIdentityOf = ( if (expression._tag === 'UnionConvert') return effectIdentityOf(expression.source, context) if (expression._tag === 'EffectBindRequirement') return effectIdentityOf(expression.protected, context) - if (expression._tag === 'EffectResult') return effectIdentityOf(expression.protected, context) if (expression._tag === 'Project') { const realization = storedEffectRealizationOf(expression, context) const represented = Type.substitute(expression.type, context.instance.substitution) @@ -454,7 +451,6 @@ const providersOf = ( } if (expression._tag === 'Move') return providersOf(expression.subject, context) if (expression._tag === 'UnionConvert') return providersOf(expression.source, context) - if (expression._tag === 'EffectResult') return providersOf(expression.protected, context) if (expression._tag !== 'EffectBindRequirement') return context.ambientProviders const proof = Instances.requirementSelection(context.instance, expression.provider) if (proof === undefined) return providersOf(expression.protected, context) @@ -895,10 +891,7 @@ const controlsOf = ( ) } } else { - const protected_ = - expression.subject._tag === 'EffectResult' - ? expression.subject.protected - : expression.subject + const protected_ = expression.subject const storedSuspendable = storedEffectRealizationOf(protected_, context)?.suspendable === true const runner = runnerOf(protected_, context) @@ -918,18 +911,15 @@ const controlsOf = ( return } if (runner.classification !== 'Synchronous') { - const policy = - expression.subject._tag === 'EffectResult' - ? reifyPolicy(runner.outcome, context) - : Object.freeze({ - _tag: 'Propagate' as const, - outcome: runner.outcome, - failureMappings: Object.freeze( - Type.failureMembers(runner.outcome).map((_failure, source) => - Object.freeze({ source: source + 1, target: source + 1 }), - ), - ), - }) + const policy = Object.freeze({ + _tag: 'Propagate' as const, + outcome: runner.outcome, + failureMappings: Object.freeze( + Type.failureMembers(runner.outcome).map((_failure, source) => + Object.freeze({ source: source + 1, target: source + 1 }), + ), + ), + }) if (policy !== undefined) { const id = controlId(execution, expression.span, idOrdinal, 'Invoke') const complete = controlId(execution, expression.span, idOrdinal, 'Complete') @@ -974,10 +964,7 @@ const providedRunnersOf = ( const visit = (expression: Hir.Expression): void => { if (expression._tag === 'EffectBlock') return if (expression._tag === 'Run') { - const protected_ = - expression.subject._tag === 'EffectResult' - ? expression.subject.protected - : expression.subject + const protected_ = expression.subject const runner = runnerOf(protected_, context) if (runner.execution._tag === 'ProvidedEffectRunnerExecution') runners.push(runner) } diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index 304f9b347..9e8f3e6fc 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -1085,12 +1085,6 @@ const collectExpression = ( for (const statement of expression.statements) collectStatement(statement, index, scope, pending) return - case 'EffectResult': - collectIntrinsicReference(expression.reference, index, pending) - collectExpression(expression.protected, index, scope, pending) - collectExpression(expression.success, index, scope, pending) - collectExpression(expression.failure, index, scope, pending) - return case 'EffectCatch': collectIntrinsicReference(expression.reference, index, pending) collectExpression(expression.protected, index, scope, pending) diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index a029c4527..82a66ada9 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -96,7 +96,7 @@ export const modules = [ module: 'silk/effect', path: 'silk/effect.silk', sourceIdentity: 'silk/effect', - digest: '70c150ae4e1d7c85ec9660f669a349a746401cdfb4e56bf3d3f73b48ad23f20a', + digest: '4ec13a79a9f01a85a700cc95b2e0171c8c395aeff65a9f4d96d39b53f2a4dede', documentation: 'silk/effect.silk', layer: 'portable', runtimeInventory: [ @@ -108,7 +108,7 @@ export const modules = [ ], namespace: 'Effect', source: - "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core executes one\n// Effect into Result data and binds one typed requirement; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because reification does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", + "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core catches typed\n// failures and binds typed requirements; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because conversion does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is converted into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", }, { module: 'silk/execution', diff --git a/packages/compiler/src/Suspension.ts b/packages/compiler/src/Suspension.ts index 0606f6552..0872a1dd2 100644 --- a/packages/compiler/src/Suspension.ts +++ b/packages/compiler/src/Suspension.ts @@ -84,7 +84,7 @@ export type SuspensionRegion = readonly ownerRegion: Mir.RegionId readonly operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' } > readonly deferred: SuspensionRunner readonly transfer: { readonly _tag: 'OriginateTransfer' } @@ -96,7 +96,7 @@ export type SuspensionRegion = readonly ownerRegion: Mir.RegionId readonly operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'ExecutionPark' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'ExecutionPark' } > readonly runner: SuspensionRunner readonly completion: SuspensionCompletion diff --git a/packages/compiler/src/SuspensionMir.ts b/packages/compiler/src/SuspensionMir.ts index 9260c3b22..55785ab1e 100644 --- a/packages/compiler/src/SuspensionMir.ts +++ b/packages/compiler/src/SuspensionMir.ts @@ -40,7 +40,7 @@ const runnerOf = ( index: DeclarationIndex.Index, operation?: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'ExecutionPark' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'ExecutionPark' } >, functions: ReadonlyArray = [], ): Mir.SuspensionRunner => { @@ -181,7 +181,7 @@ interface LocatedOperation { readonly region: Mir.RegionId readonly operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'ExecutionPark' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'ExecutionPark' } > } @@ -200,7 +200,7 @@ const operationsOf = (fn: Mir.MirFunction): ReadonlyArray => .flatMap((operation) => operation._tag === 'RunEffect' || operation._tag === 'RunEffectValue' || - operation._tag === 'ReifyEffect' || + operation._tag === 'CatchEffect' || operation._tag === 'ExecutionPark' ? [Object.freeze({ region: region.id, operation })] : [], @@ -255,8 +255,8 @@ const regionsOf = ( (entry) => sameSpan(entry.operation, outcome) && (outcome.completion._tag === 'Reify' - ? entry.operation._tag === 'ReifyEffect' - : entry.operation._tag !== 'ReifyEffect') && + ? entry.operation._tag === 'CatchEffect' + : entry.operation._tag !== 'CatchEffect') && (entry.operation._tag === 'ExecutionPark' ? Type.equals(outcome.runner.outcome.success, Type.unit) : true), diff --git a/packages/compiler/src/SuspensionOwnership.ts b/packages/compiler/src/SuspensionOwnership.ts index 233e2414d..af01ec634 100644 --- a/packages/compiler/src/SuspensionOwnership.ts +++ b/packages/compiler/src/SuspensionOwnership.ts @@ -162,7 +162,7 @@ const operationDefinitions = (operation: Mir.Operation): ReadonlySet => nested._tag === 'RunEffect' || nested._tag === 'RunEffectValue' || nested._tag === 'RunStaticEffect' || - nested._tag === 'ReifyEffect' || + nested._tag === 'CatchEffect' || nested._tag === 'CloseEffectEntry' ) definitions.add(nested.outcome.ordinal) @@ -571,7 +571,7 @@ export const plan = ( if ( operation._tag !== 'RunEffect' && operation._tag !== 'RunEffectValue' && - operation._tag !== 'ReifyEffect' && + operation._tag !== 'CatchEffect' && operation._tag !== 'ExecutionPark' ) continue diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 95eee04bc..ffb34ac94 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '309f8978492aca399dd0e3d562322755aecfe2d16d9457985b8ef046d5047706' +export const compilerDigest = 'f9b5ea29e7049585ec1eb8083531948bae405ee950125e5c4298a0aa6ab5dd5e' diff --git a/packages/compiler/src/Type.ts b/packages/compiler/src/Type.ts index eb1e063e3..9719f3a48 100644 --- a/packages/compiler/src/Type.ts +++ b/packages/compiler/src/Type.ts @@ -531,12 +531,6 @@ export const storageFailure: Nominal = sealedStorageFailure() export const some = (element: Type): Nominal => nominal('silk/option', 'Some', [element]) export const none: Nominal = nominal('silk/option', 'None') -/** Canonical completed Effect outcome data shipped by silk/result. */ -export const resultSuccess = (value: Type): Nominal => nominal('silk/result', 'Success', [value]) -export const resultFailure = (error: Type): Nominal => nominal('silk/result', 'Failure', [error]) -export const result = (value: Type, error: Type): Nominal => - nominal('silk/result', 'Result', [value, error]) - /** Normalizes one or more ordinary failure types to their runtime value union. */ export const failureValue = (failures: ReadonlyArray): Type => { const only = failures.at(0) diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 3407eeb51..fa11d9c96 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -1411,7 +1411,7 @@ const layoutOf = ( const suspensionOperationInputs = ( operation: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'ExecutionPark' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'ExecutionPark' } >, ): ReadonlyArray => { switch (operation._tag) { @@ -1420,7 +1420,7 @@ const suspensionOperationInputs = ( case 'RunEffect': return operation.arguments case 'RunEffectValue': - case 'ReifyEffect': + case 'CatchEffect': return Object.freeze([operation.effect, ...operation.arguments]) } } @@ -1429,13 +1429,13 @@ const matchesSuspensionOperation = ( candidate: Mir.Operation, expected: Extract< Mir.Operation, - { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'ReifyEffect' | 'ExecutionPark' } + { readonly _tag: 'RunEffect' | 'RunEffectValue' | 'CatchEffect' | 'ExecutionPark' } >, ): boolean => candidate === expected || ((candidate._tag === 'RunEffect' || candidate._tag === 'RunEffectValue' || - candidate._tag === 'ReifyEffect' || + candidate._tag === 'CatchEffect' || candidate._tag === 'ExecutionPark') && candidate._tag === expected._tag && candidate.destination.ordinal === expected.destination.ordinal && @@ -6122,8 +6122,8 @@ const emitRunEffectCompositeOperation = ( ] } -const emitReifyEffectOperation = ( - operation: Extract, +const emitCatchEffectOperation = ( + operation: Extract, state: WasmOperationContext, ): ReadonlyArray => { const { @@ -7387,8 +7387,8 @@ const emitOperationWithContext = ( return emitRunEffectValueOrRunStaticEffectOperation(operation, context) case 'RunEffectComposite': return emitRunEffectCompositeOperation(operation, context) - case 'ReifyEffect': - return emitReifyEffectOperation(operation, context) + case 'CatchEffect': + return emitCatchEffectOperation(operation, context) case 'CloseEffectEntry': return emitCloseEffectEntryOperation(operation, context) case 'Call': diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 6232a4370..677b0799b 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -226,7 +226,7 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan ...operation.arguments, ]) || changed break - case 'ReifyEffect': + case 'CatchEffect': changed = include(operation.destination, [operation.effect, ...operation.arguments]) || changed break diff --git a/packages/compiler/stdlib/silk/effect.silk b/packages/compiler/stdlib/silk/effect.silk index d20430bd9..922e9c8a7 100644 --- a/packages/compiler/stdlib/silk/effect.silk +++ b/packages/compiler/stdlib/silk/effect.silk @@ -21,7 +21,7 @@ //! owned provider bindings have distinct borrowing and capture behavior. //! //! # Gotchas -//! Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass +//! Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass //! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary //! while preserving all three channels exactly; frame exhaustion is fatal. //! @@ -116,8 +116,8 @@ //! } //! ``` -// Familiar channel transformations derived from the closed compiler core. The core executes one -// Effect into Result data and binds one typed requirement; everything here is ordinary Silk. +// Familiar channel transformations derived from the closed compiler core. The core catches typed +// failures and binds typed requirements; everything here is ordinary Silk. import silk.bool as bool import silk.logger { LogError, LogLevel, Logger } @@ -198,7 +198,7 @@ pub effect fn logError( /// /// # Details /// -/// The returned Effect still requires `R`, because reification does not provide services. Its typed +/// The returned Effect still requires `R`, because conversion does not provide services. Its typed /// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed /// failures and therefore are not captured. /// @@ -439,7 +439,7 @@ where S in E { /// /// # Details /// -/// The protected Effect is reified into Result data before the finalizer runs, which is what fixes +/// The protected Effect is converted into Result data before the finalizer runs, which is what fixes /// the order: a typed failure reaches this body as data rather than as a propagation, so the /// protected Effect's own frame — and every local it cleans up — is already gone by the time the /// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the diff --git a/packages/compiler/test/EffectRuntime.test.ts b/packages/compiler/test/EffectRuntime.test.ts index 4c3128e82..06318ab5b 100644 --- a/packages/compiler/test/EffectRuntime.test.ts +++ b/packages/compiler/test/EffectRuntime.test.ts @@ -822,7 +822,7 @@ it.effect('resolves flatten through the ordinary declaration path without an int const catalog = Intrinsic.all().flatMap((actor) => actor.operations.map((operation) => operation.spelling), ) - assert.include(catalog, 'effectResult') + assert.notInclude(catalog, 'effectResult') assert.notInclude(catalog, 'flatten') }), ) diff --git a/packages/compiler/test/MirNormalization.test.ts b/packages/compiler/test/MirNormalization.test.ts index 70ea5f503..3670ecffd 100644 --- a/packages/compiler/test/MirNormalization.test.ts +++ b/packages/compiler/test/MirNormalization.test.ts @@ -153,17 +153,18 @@ it.effect('retains only the exact execution whose runner fact is unknown', () => }), ) -it.effect('retains suspendable reification and effect-entry closure control', () => +it.effect('retains suspendable catch and effect-entry closure control', () => Effect.gen(function* () { - const reified = yield* Analysis.ofSourceRealized( - 'test/mir-normalization-reify-suspendable', + const caught = yield* Analysis.ofSourceRealized( + 'test/mir-normalization-catch-suspendable', encoder.encode(`import silk.effect as Effect -effect fn seed(value: i32) -> i32 { +struct Problem {} +effect fn seed(value: i32) -> i32 ! Problem { return run Effect.suspend(effect { return value }) } -fn increment(value: i32) -> i32 { return value + 1 } +effect fn recover(problem: Problem) -> i32 { return 0 } pub fn main() -> i32 { - return run seed(41) |> Effect.map(increment) + return run seed(42) |> Effect.catchAll(recover) }`), 'wasm32-unknown-unknown', ) @@ -173,9 +174,9 @@ pub fn main() -> i32 { 'wasm32-unknown-unknown', { normalizeMir: false }, ) - assert.deepEqual(Analysis.diagnostics(reified), []) + assert.deepEqual(Analysis.diagnostics(caught), []) assert.deepEqual(Analysis.diagnostics(entry), []) - const reifiedProgram = Analysis.loweredMir(reified) + const caughtProgram = Analysis.loweredMir(caught) const rawEntryProgram = Analysis.loweredMir(entry) const closure = allOperations(rawEntryProgram).find( (operation): operation is Extract => @@ -198,36 +199,15 @@ pub fn main() -> i32 { }) const entryProgram = MirNormalization.normalize(rawEntryProgram, suspendableEntryFacts) assert.isTrue( - allOperations(reifiedProgram).some((operation) => operation._tag === 'ReifyEffect'), - MirEncoding.encode(reifiedProgram), - ) - const reify = allOperations(reifiedProgram).find( - (operation): operation is Extract => - operation._tag === 'ReifyEffect', - ) - assert.isDefined(reify) - assert.isFalse(allOperations(reifiedProgram).some((operation) => operation._tag === 'Allocate')) - if (reify === undefined) return - for (const field of ['successTag', 'failureTag'] as const) { - for (const tag of [-1, reify.resultUnion.members.length, Number.MAX_SAFE_INTEGER + 1]) { - const forged = structuredClone(reifiedProgram) - const operation = allOperations(forged).find( - (candidate) => candidate._tag === 'ReifyEffect', - ) - assert.isDefined(operation) - if (operation === undefined) return - Reflect.set(operation, field, tag) - assert.include( - MirVerification.verify(forged).map((violation) => violation.rule), - 'InvalidEffectOperation', - ) - } - } + allOperations(caughtProgram).some((operation) => operation._tag === 'CatchEffect'), + MirEncoding.encode(caughtProgram), + ) + assert.isFalse(allOperations(caughtProgram).some((operation) => operation._tag === 'Allocate')) assert.isTrue( allOperations(entryProgram).some((operation) => operation._tag === 'CloseEffectEntry'), MirEncoding.encode(entryProgram), ) - for (const program of [reifiedProgram, entryProgram]) { + for (const program of [caughtProgram, entryProgram]) { assert.isTrue( (program.normalization ?? []).some( (verdict) => verdict._tag === 'Rejected' && verdict.reason === 'SuspendableRunner', @@ -263,7 +243,7 @@ pub fn main() -> i32 { assert.isTrue( allOperations(program).some( (operation) => - (operation._tag === 'RunEffectValue' || operation._tag === 'ReifyEffect') && + (operation._tag === 'RunEffectValue' || operation._tag === 'CatchEffect') && operation.runner.name.includes('$provided$'), ), MirEncoding.encode(program), diff --git a/packages/compiler/test/ZipAcceptance.test.ts b/packages/compiler/test/ZipAcceptance.test.ts index b80019677..32be4be4e 100644 --- a/packages/compiler/test/ZipAcceptance.test.ts +++ b/packages/compiler/test/ZipAcceptance.test.ts @@ -350,7 +350,7 @@ it.effect('resolves zip and zip3 through the ordinary declaration path without a const catalog = Intrinsic.all().flatMap((actor) => actor.operations.map((operation) => operation.spelling), ) - assert.include(catalog, 'effectResult') + assert.notInclude(catalog, 'effectResult') assert.notInclude(catalog, 'zip') assert.notInclude(catalog, 'zip3') }), diff --git a/packages/compiler/test/fixtures/intrinsic-inventory.json b/packages/compiler/test/fixtures/intrinsic-inventory.json index ae33d5ffd..52248116e 100644 --- a/packages/compiler/test/fixtures/intrinsic-inventory.json +++ b/packages/compiler/test/fixtures/intrinsic-inventory.json @@ -5420,14 +5420,6 @@ "consumer": "silk/effect.suspendEffect", "identity": "EffectSuspend" }, - { - "operation": "Intrinsic.effectResult", - "signature": "fn Intrinsic.effectResult(protected: Effect, success: once fn(A) -> R, failure: once fn(E) -> R) -> Effect", - "unsafe": false, - "admission": "Effect", - "consumer": "silk/effect.result", - "identity": "EffectRule.Result" - }, { "operation": "Intrinsic.bindRequirement", "signature": "fn Intrinsic.bindRequirement(protected: once Effect, provider: &P) -> Effect>", diff --git a/packages/compiler/test/fixtures/synchronous-effect-cost.mjs b/packages/compiler/test/fixtures/synchronous-effect-cost.mjs index bd560ff70..5cd1ee626 100644 --- a/packages/compiler/test/fixtures/synchronous-effect-cost.mjs +++ b/packages/compiler/test/fixtures/synchronous-effect-cost.mjs @@ -512,7 +512,7 @@ const runnerClassifications = (program) => { 'RunEffect', 'RunEffectValue', 'RunStaticEffect', - 'ReifyEffect', + 'CatchEffect', 'CloseEffectEntry', ].includes(candidate._tag), ).length diff --git a/packages/compiler/test/goldens/effect.mir.txt b/packages/compiler/test/goldens/effect.mir.txt index 873077d2a89f0a8d6da840c323b9a18108b2a037..e21f0768b5568aeed75036ed891304abbefca70b 100644 GIT binary patch delta 458 zcmZ9Iu}{K46vlHwLxD;l2!mj91Pc#eNbM`ViMy<1Do7{PV5H8>jSt*=Aa;#;5il# z$B6_Ak^F<;DJ{cYx(c@$Q=+7Sb(LxGmZ?dU3ZYRZL^VOUCKTa16tgv%Pc4J9deU~_ zTP}xgHn+ArU#QiSSd3@+f#+aF#~pLYiW1ubLJB|fMYLn8Iuo2K6>Y1rxfQ0iUJ>H( zmEYeaxQ)HQX6+=XZ?#RLliCCul7Ow4j z*IloVJ#E>8_fi>AnBW33AYVT3b4}cAa?<*vRG8?TA-I9xbB7l8yO(VhD-<8#;|T6h Z;2eq8mF)a6?7y{mOdKZJ6ns|he*tbwcvJuY literal 10138 zcmeHNTW{OQ6%L9%`BzNWD-aZL=7*ive4Mhq=%7z#H@jYiQ z)C@^cii2!ll7~bgXU?3ov=o8Q+&pzGd$+S`_r5V6!dVU9hs@ zFQnqDyrl4>p?t%dbY#c%mgXhd((ExW*~x_$WyQC&$p4^CUX_Gq8QV2%MZV_cD*aRy zE4KPjm371SSyOSc-j^9JJV}>uw!wAWAw>lOr4yHI_LQ&c^zS(bX@3`jOz-n@Q?Sos z@$~+s%pQ4FR{NSPU4Jnp?%cWh^H>aC#bOk7S>)_LAR%axGg^ZZJ_`GOtTVb}_4n!U zOtnZI-#1{@?DM|-o-@NUlp>7y@0ytwoPMZwFaP5AkCcNZfbTh&^VPbl^Wy6}iFyF| zt`9GdEaPC|Oi3Kg-5+3(dJnE4OxnZsGLAtsXa34lP0={3_eIl}&Lzvpi$H^s@4r?$ z>l~WbZ1@;I3hd;5?0rrXkP3&yD8Zd`~3Rra`(+maQ_3d%*vl_*L>#b2VfC3W7g1mdUCh}MMgzdGFksV#$e;UpR4+(ZEW z&KcrS=tQ1~X>^DmV@U!_?6VJs?4MnWOs`L$wp_W1dZX*wyWkXS9MjV&=ENeKy#vPk z>^1Z&)6AVbUxcHR42V@W2M1s5b6_jzLaSqTE{L$Oztg+U#t`n3Jh}w>4VFqaj$jE& z)CN+7GrQfn5>5KRJBK{w>^aSV#+@?-rtjjZ0HhoK`LcMgl0|vO2|*yei3D5FlGUk; zzn5Qxi1_BCDCT8$kSjc~0>aXlDK1s8r$UGLCGu zyjNw$UMGOL^M^@c9&$<)lZj3?lHxn9)aKH@)|Jww5sjsn-nDbx8nydk$jHW;Yb$q{ z_AnM*I@iv@HZoO@@!k=0Bk7nKpf-|_>4SZPc2ElpD{W)~}o*zQXZ&P;%sRtoa4?>{6 zF3Ct5l@k$>k7}q(#XO=;BQm=cZZ0M)FsOhs0Hzhy4pf38js*Ts$iyKj(R%VoB6cj@ z06RWZgIMFjnk}T4nPg&=7nRbDrbva{I-SV;4TqR9#_0IPVV=QI;Ite?pbR&H9#kyy zHAgyn%fbf2(>2uZq&@Od2KtF7Vm%OBmv3RwmDbJD4Mp1y&7hzg0$r$7X0@0sXnEmD z3Ehf-wp0NY7lR_|VqiM)Rarn_5VXevs+L{Gj#oR19Z1^B){YL42hJctm1Gi{GX(%Z z=XBXifGnz^n3CVfR9QwP_Q8SM8)cw3e|gyFMFY7LXuOeqgR0e%dr|4!7z_U9I?3ni z`F*ir51jgb5+^V=%YawrImsgVl$YRZup&z4$$QS}OZ(%u*KfW3tJL%TZsCse= zK#-%xyE7>WSCZ9)msHPab;-h@F2(Ab+gqsUp+c=wPy3#a_%cC0?t=(<)4w{V9t4`B z;5D6S$aVe792c3u>P(VO#1;h?EuyPM^uA5R9~(m20hK8y%#_;a+JAI_7&I8XglPnf zqmKqlG!({{2~0d-;H#o#6ouf0QM7=ejmZEg+I*#XP!UABoG}vk>A~2I$uXq%;MTPU zac9&T*Zsoh_JlFJkTktc8fVn;4(@h}4QsS%0;Dtb>;eRvL_Nz=fVI^yP*0BIcT{9N z?uf+LTY|g;=PjY6YUin4*^E~m*{t;WGnMu4#k!90e@kU<_-%ncIJbg2$Hm4J9DL;jd#W}EF0ti^BVB9jQC)q4fJiiiKv|R5IBYy|Tt;Eaz5+Sw z^b#8Z{6qPcT-syAeh?)O3{(R{6=S+Qu!^p6oX@tq<^?q$;w45DsA?S#dw}~^JPpd) zn;HJFHFSRKhqp9!o&EzA4JXCW7R2*KEHf{jRI@@}PMRwv*x-`Z$NPraLj@E4=X6br znkjAIR(#!>Gjx1q&hXXDwpIpWU5(&@ax81x+E&HXvh2Mvx;OYff&-fp)WQc0xTklU zXIXnSeo%lx6j?h5@MH4@Gq%IUg|$_J%6x6_1p9{`A}x&JidwRx|8DUXwRpqw&cp=l z#{NAH=LUM@{!t8xE30D=lKUqy;0SNOUS_%j0dT#YL|2E_R9rbJe$>d4;rvcC=jjJ% zTp0WPk(KXt`v{$3`yoU6(HP9DGBgfxV9-~F@N;|e-_heq8xh;TLEngGU(E-!&J8bL zv|5*AA6xH{6XJeX)ys$dqk4C-IzQ^WO7SkvGf?N!Z*;9~lu~aei>o8GOdNS4821Ik z*nq`9&IVLNC}yqj*ujV0@qn1!8^shf{ZILdaWw-N%WG0$OHhxcAzI|dBl6FgVte109@dLfp ps4F!bz7qOnUqVvI9>w^2Ex&$%%X9gO!7sp@9|>(Nf-9qv{U3uiE>-{l From 2a3eb5f307bca6b4ca5aa2f79ad90a5619f89c93 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 17:40:38 -0300 Subject: [PATCH 18/42] Make recoverable host operations carrier neutral --- openspec/changes/add-nominal-unions/tasks.md | 20 +-- packages/compiler/src/BootstrapEvaluation.ts | 152 +++++------------- .../compiler/src/BootstrapOsIntrinsics.ts | 37 ++--- packages/compiler/src/ExpressionAnalysis.ts | 6 +- .../compiler/src/InspectorProjectBackend.ts | 2 + packages/compiler/src/Intrinsic.ts | 49 +++++- packages/compiler/src/LowerExpression.ts | 39 +++++ packages/compiler/src/Mir.ts | 16 ++ packages/compiler/src/MirEncoding.ts | 2 + packages/compiler/src/MirLinearization.ts | 97 +++++++++++ packages/compiler/src/MirVerification.ts | 66 ++++++++ .../compiler/src/NativeMemoryOperation.ts | 78 +++++---- packages/compiler/src/NativeOperation.ts | 1 + packages/compiler/src/NativeProgram.ts | 17 +- packages/compiler/src/Stdlib.generated.ts | 4 +- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/Type.ts | 30 ---- packages/compiler/src/WasmBackend.ts | 3 +- .../compiler/stdlib/silk/os_filesystem.silk | 4 +- packages/compiler/test/IntegerScalars.test.ts | 69 ++++++++ .../compiler/test/IntrinsicCatalog.test.ts | 8 +- packages/compiler/test/OsFileSystem.test.ts | 80 +++++++++ .../test/fixtures/intrinsic-inventory.json | 8 +- 23 files changed, 553 insertions(+), 237 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index ed115adfc..221c84be0 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -67,21 +67,21 @@ ## 9. Carrier-Neutral Intrinsic Migration -- [ ] 9.1 Replace checked scalar intrinsic result contracts with generic present/absent exact `once fn` carriers while keeping the intrinsic operation inventory count unchanged, and verify catalog audit tests contain no Option identity or spelling. -- [ ] 9.2 Lower and execute checked carrier selection with exactly one callback invocation and cleanup of the unused callable environment, and verify evaluator, Wasm, and native tests cover success, absence, affine captures, and traps. +- [x] 9.1 Replace checked scalar intrinsic result contracts with generic present/absent exact `once fn` carriers while keeping the intrinsic operation inventory count unchanged, and verify catalog audit tests contain no Option identity or spelling. +- [x] 9.2 Lower and execute checked carrier selection with exactly one callback invocation and cleanup of the unused callable environment, and verify evaluator, Wasm, and native tests cover success, absence, affine captures, and traps. - [x] 9.3 Delete `Intrinsic.effectResult` and its analysis, HIR, MIR, evaluator, Wasm, LLVM, layout-discovery, and suspension-metadata support without replacement, and verify the intrinsic inventory plus repository searches contain no completed-outcome primitive or compatibility path. -- [ ] 9.4 Replace handle-producing file and directory open results with affine-safe success/failure `once fn` carriers, and verify success transfers one initialized `OsHandle` plus close obligation while failure creates no handle or optionally initialized place. -- [ ] 9.5 Replace optional count-producing OS filesystem, standard-input, child-process, and process-input results with primitive `bool` plus initialized count/reason/code outputs, and verify host-boundary tests distinguish zero-length success, absence, and refusal without constructing Option in compiler code. -- [ ] 9.6 Remove `Type.option`, old Result/member helpers, detached outcome construction, and Option/Result-specific branches from analysis, HIR, MIR, evaluation, and backends, and verify repository searches plus intrinsic audits find no compiler recognition by standard-library module or declaration spelling. +- [x] 9.4 Replace handle-producing file and directory open results with affine-safe success/failure `once fn` carriers, and verify success transfers one initialized `OsHandle` plus close obligation while failure creates no handle or optionally initialized place. +- [x] 9.5 Replace optional count-producing OS filesystem, standard-input, child-process, and process-input results with primitive `bool` plus initialized count/reason/code outputs, and verify host-boundary tests distinguish zero-length success, absence, and refusal without constructing Option in compiler code. +- [x] 9.6 Remove `Type.option`, old Result/member helpers, detached outcome construction, and Option/Result-specific branches from analysis, HIR, MIR, evaluation, and backends, and verify repository searches plus intrinsic audits find no compiler recognition by standard-library module or declaration spelling. ## 10. Atomic Standard-Library Migration -- [ ] 10.1 Replace `option.silk` with the public nominal union and direct `some`/`none` helpers, and verify its combinators construct and match direct variants with public payload access and no wrapper field. -- [ ] 10.2 Replace `result.silk` with the public nominal union and direct `succeed`/`failResult` helpers, and verify its combinators accept structural error unions without flattening Success or Failure. -- [ ] 10.3 Update integer, character, string, allocation, and other checked wrappers to supply carrier-neutral intrinsic adapters and return direct Option variants, and verify checked success/absence tests use the canonical nominal representation. +- [x] 10.1 Replace `option.silk` with the public nominal union and direct `some`/`none` helpers, and verify its combinators construct and match direct variants with public payload access and no wrapper field. +- [x] 10.2 Replace `result.silk` with the public nominal union and direct `succeed`/`failResult` helpers, and verify its combinators accept structural error unions without flattening Success or Failure. +- [x] 10.3 Update integer, character, string, allocation, and other checked wrappers to supply carrier-neutral intrinsic adapters and return direct Option variants, and verify checked success/absence tests use the canonical nominal representation. - [x] 10.4 Implement ordinary Silk `Effect.result` by mapping success into `Result.Success` and applying general `Effect.catchAll` to map the complete typed failure into `Result.Failure`; migrate direct consumers and verify one nominal layer, compound failure unions, preserved requirements, move-only branch values, and an equivalent user-defined result-like union. -- [ ] 10.5 Migrate filesystem, process, formatting, random, collection, and remaining canonical Silk modules from detached member imports and wrapper-field matches to qualified parent variants, and verify the complete stdlib source closure compiles. -- [ ] 10.6 Delete detached `Some`, `None`, `Success`, and `Failure` declarations, wrapper structs, aliases, dual paths, stale imports, and old generated embeddings, then regenerate the deterministic stdlib manifest and verify a repository-wide removal test finds no superseded representation. +- [x] 10.5 Migrate filesystem, process, formatting, random, collection, and remaining canonical Silk modules from detached member imports and wrapper-field matches to qualified parent variants, and verify the complete stdlib source closure compiles. +- [x] 10.6 Delete detached `Some`, `None`, `Success`, and `Failure` declarations, wrapper structs, aliases, dual paths, stale imports, and old generated embeddings, then regenerate the deterministic stdlib manifest and verify a repository-wide removal test finds no superseded representation. ## 11. Tooling, Documentation, and Acceptance diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index a78f11aa8..2ae4e91c0 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -1123,95 +1123,6 @@ function* executeFunction( ) return Object.freeze({ _tag: 'Value', value: floatValue(floatTarget.spelling, encoded.bits) }) } - if (operation === 'CheckedConvertToChar') { - const subject = arguments_.at(0) - if (actorScalar?.category !== 'Character' || subject?._tag !== 'IntegerValue') - throw new RangeError('MIR verifier allowed an invalid checked char callable') - const exact = BigInt(subject.value) - const succeeded = Scalar.isUnicodeScalarValue(exact) - const semantic = Type.option('char') - if (!Type.isUnion(semantic)) - throw new RangeError('Canonical Option did not normalize to a structural union') - const member = succeeded ? Type.some('char') : Type.none - const entry = program.layout.entries.find((candidate) => Type.equals(candidate.type, member)) - if (entry?._tag !== 'LayoutEntry' || entry.representation._tag !== 'Aggregate') - throw new RangeError('Target plan omitted a canonical callable Option member') - return Object.freeze({ - _tag: 'Value', - value: Object.freeze({ - _tag: 'UnionValue', - type: semantic, - member, - payload: Object.freeze({ - _tag: 'AggregateValue', - type: member, - fields: Object.freeze( - succeeded - ? entry.representation.fields.map((field) => - Object.freeze({ field: field.id, value: characterValue(Number(exact)) }), - ) - : [], - ), - }), - }), - }) - } - if (Scalar.isCheckedOperation(operation)) { - const source = Scalar.find(target.actor) - const resultScalar = conversionTarget ?? source - const leftValue = arguments_.at(0) - const rightValue = arguments_.at(1) - if ( - source?.category !== 'Integer' || - resultScalar?.category !== 'Integer' || - leftValue === undefined || - leftValue._tag !== 'IntegerValue' - ) - throw new RangeError('MIR verifier allowed an invalid checked callable') - const left = BigInt(leftValue.value) - const right = - rightValue !== undefined && rightValue._tag === 'IntegerValue' - ? BigInt(rightValue.value) - : undefined - const pointerBits = program.layout.target.pointerSize === 4 ? 32 : 64 - const exact = BootstrapArithmetic.checked( - operation, - left, - right, - Scalar.range(source, pointerBits).minimum, - ) - const range = Scalar.range(resultScalar, pointerBits) - const succeeded = exact !== undefined && exact >= range.minimum && exact <= range.maximum - const semantic = Type.option(resultScalar.spelling) - if (!Type.isUnion(semantic)) - throw new RangeError('Canonical Option did not normalize to a structural union') - const member = succeeded ? Type.some(resultScalar.spelling) : Type.none - const entry = program.layout.entries.find((candidate) => Type.equals(candidate.type, member)) - if (entry?._tag !== 'LayoutEntry' || entry.representation._tag !== 'Aggregate') - throw new RangeError('Target plan omitted a canonical callable Option member') - return Object.freeze({ - _tag: 'Value', - value: Object.freeze({ - _tag: 'UnionValue', - type: semantic, - member, - payload: Object.freeze({ - _tag: 'AggregateValue', - type: member, - fields: Object.freeze( - succeeded - ? entry.representation.fields.map((field) => - Object.freeze({ - field: field.id, - value: integerValue(resultScalar.spelling, exact), - }), - ) - : [], - ), - }), - }), - }) - } if (conversionTarget !== undefined) { const subject = arguments_.at(0) if (actorScalar?.category === 'Character' && subject?._tag === 'CharacterValue') @@ -1479,31 +1390,6 @@ function* executeFunction( state.cells.set(key, { value: next, fromCall: backing.fromCall }) } - const optionValue = (element: Type.Type, payload?: Value): UnionValue => { - const semantic = Type.option(element) - if (!Type.isUnion(semantic)) throw new RangeError('OS Option result did not normalize') - const member = payload === undefined ? Type.none : Type.some(element) - const entry = program.layout.entries.find((candidate) => Type.equals(candidate.type, member)) - if (entry?._tag !== 'LayoutEntry' || entry.representation._tag !== 'Aggregate') - throw new RangeError('Target plan omitted an OS Option member') - return Object.freeze({ - _tag: 'UnionValue', - type: semantic, - member, - payload: Object.freeze({ - _tag: 'AggregateValue', - type: member, - fields: Object.freeze( - payload === undefined - ? [] - : entry.representation.fields.map((field) => - Object.freeze({ field: field.id, value: payload }), - ), - ), - }), - }) - } - const handleValue = (handle: OsFileSystemHost.Handle): AggregateValue => { const entry = program.layout.entries.find((candidate) => Type.equals(candidate.type, Type.osHandle), @@ -2589,6 +2475,7 @@ function* executeFunction( break } case 'HostWrite': + case 'OsOpen': case 'OsCall': { const boundary = BootstrapOsIntrinsics.execute( { @@ -2602,13 +2489,48 @@ function* executeFunction( replaceReferenced, byteView, writeByteView, - optionValue, handleValue, hostHandle, }, operation, ) if (boundary !== undefined) return boundary + if (operation._tag === 'OsOpen') { + const succeeded = readInteger(operation.valid).value !== 0n + const callable = succeeded ? operation.success : operation.failure + const unused = succeeded ? operation.failure : operation.success + const cleanup = succeeded ? operation.failureCleanup : operation.successCleanup + const callableType = fn.localTypes.at(callable.ordinal) + if (callableType?._tag !== 'CallableValue') + throw new RangeError('MIR OS open lost its carrier callable') + const carrier = yield* executeOperations([ + Object.freeze({ + _tag: 'Drop' as const, + local: unused, + cleanup, + provenance: operation.provenance, + }), + Object.freeze({ + _tag: 'ApplyCallable' as const, + destination: operation.destination, + callable, + typeArguments: + callableType.environment?.callable.typeArguments ?? + callableType.storage?.realization.targetArguments ?? + callableType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: succeeded ? Object.freeze([operation.handle]) : Object.freeze([]), + callableType: callableType.type, + access: callableType.type.mode, + evaluation: 'CalleeThenArguments' as const, + realization: 'Environment' as const, + type: operation.type, + provenance: operation.provenance, + }), + ]) + if (carrier !== undefined) return carrier + } break } case 'RawBufferFrom': { diff --git a/packages/compiler/src/BootstrapOsIntrinsics.ts b/packages/compiler/src/BootstrapOsIntrinsics.ts index 21aa7be83..e7ba7ba46 100644 --- a/packages/compiler/src/BootstrapOsIntrinsics.ts +++ b/packages/compiler/src/BootstrapOsIntrinsics.ts @@ -1,11 +1,5 @@ import type { BlockedReason, TraceEvent } from './BootstrapTrace.js' -import type { - AggregateValue, - IntegerValue, - SliceValue, - UnionValue, - Value, -} from './BootstrapValue.js' +import type { AggregateValue, IntegerValue, SliceValue, Value } from './BootstrapValue.js' import type * as ChildProcess from './ChildProcess.js' import type * as HostInput from './HostInput.js' import type * as Mir from './Mir.js' @@ -84,7 +78,6 @@ export interface ExecutionContext { readonly replaceReferenced: (local: Mir.LocalId, replacement: Value) => void readonly byteView: (local: Mir.LocalId) => ReadonlyArray readonly writeByteView: (local: Mir.LocalId, bytes: ReadonlyArray) => void - readonly optionValue: (element: Type.Type, payload?: Value) => UnionValue readonly handleValue: (handle: OsFileSystemHost.Handle) => AggregateValue readonly hostHandle: (local: Mir.LocalId) => OsFileSystemHost.Handle } @@ -98,7 +91,7 @@ const blockedStep = (reason: BlockedReason): BoundaryStep => /** Executes the host/OS boundary operations owned by the bootstrap OS actor. */ export const execute = ( context: ExecutionContext, - operation: Extract, + operation: Extract, ): BoundaryStep | undefined => { const { state, @@ -111,7 +104,6 @@ export const execute = ( replaceReferenced, byteView, writeByteView, - optionValue, handleValue, hostHandle, } = context @@ -180,10 +172,14 @@ export const execute = ( }) break } + case 'OsOpen': case 'OsCall': { const arguments_ = operation.arguments const commit = (result: Value): void => - write(operation.destination, { value: result, fromCall: false }) + write(operation._tag === 'OsOpen' ? operation.valid : operation.destination, { + value: result, + fromCall: false, + }) const name = operation.operation.name const clockResult = ( completed: boolean, @@ -589,10 +585,13 @@ export const execute = ( : invoke(() => host.directoryOpen(byteView(root), byteView(path))) if (result._tag === 'Failure') { status(result) - commit(optionValue(Type.osHandle)) + commit(integerValue('i32', 0)) } else { status() - commit(optionValue(Type.osHandle, handleValue(result.handle))) + if (operation._tag !== 'OsOpen') + throw new RangeError('OS handle open lost its affine carrier operation') + write(operation.handle, { value: handleValue(result.handle), fromCall: false }) + commit(integerValue('i32', 1)) } break } @@ -780,17 +779,7 @@ export const execute = ( } catch (cause) { const failure = osFailure(cause) status(failure) - commit( - operation.type._tag === 'Union' - ? optionValue( - operation.type.type.members.some((member) => - Type.equals(member, Type.some(Type.osHandle)), - ) - ? Type.osHandle - : 'usize', - ) - : integerValue('i32', 0), - ) + commit(integerValue('i32', 0)) } break } diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index 121ea2472..aa07696a6 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -1101,7 +1101,10 @@ export const intrinsicStruct = ( name: type.name, }), }), - visibility: 'Public', + visibility: + Type.equals(type, Type.layout) || Type.equals(type, Type.invalidAlignment) + ? 'Public' + : 'Private', typeParameters: Object.freeze([]), name: Object.freeze({ _tag: 'Present', spelling: type.name, token }), fields: Object.freeze( @@ -2742,6 +2745,7 @@ export const analyzeAggregateLiteral = ( const authorized = definingModule !== undefined && aggregate !== undefined && + (aggregate.visibility === 'Public' || definingModule === source.id) && aggregateFields.every((field) => field.visibility === 'Public' || definingModule === source.id) const accessDiagnostic = nominal !== undefined && !authorized diff --git a/packages/compiler/src/InspectorProjectBackend.ts b/packages/compiler/src/InspectorProjectBackend.ts index 9a37f269b..456bc82ea 100644 --- a/packages/compiler/src/InspectorProjectBackend.ts +++ b/packages/compiler/src/InspectorProjectBackend.ts @@ -891,6 +891,8 @@ const operationLabel = (operation: Mir.Operation): string => { return `${localText(operation.destination)} = ${operation.operator === 'And' ? '&&' : '||'} ${localText(operation.left)}` case 'HostWrite': return `${localText(operation.destination)} = write all ${localText(operation.bytes)} to stream ${localText(operation.stream)} ! ${operation.failure.name}` + case 'OsOpen': + return `${localText(operation.destination)} = ${operation.operation}(${operation.arguments.map(localText).join(', ')}) via ${localText(operation.success)}/${localText(operation.failure)}` case 'OsCall': return `${localText(operation.destination)} = ${operation.operation}(${operation.arguments.map(localText).join(', ')})` case 'SharedFromAllocation': diff --git a/packages/compiler/src/Intrinsic.ts b/packages/compiler/src/Intrinsic.ts index eba887754..81178515b 100644 --- a/packages/compiler/src/Intrinsic.ts +++ b/packages/compiler/src/Intrinsic.ts @@ -620,6 +620,42 @@ const osBuiltin = (options: { invariant: options.invariant, }) +const osOpen = (options: { + readonly name: 'fileOpen' | 'directoryOpen' + readonly operation: 'OsFileOpen' | 'OsDirectoryOpen' + readonly parameters: ReadonlyArray + readonly semanticParameters: ReadonlyArray + readonly invariant: string +}): Operation => { + const carrierOwner = Object.freeze({ + module: 'Intrinsic', + name: `$Os.${options.name}`, + }) + const carrierResult = Type.parameter(carrierOwner, 0, 'R') + const success = Type.callable(Object.freeze([Type.osHandle]), carrierResult, 'Take') + const failure = Type.callable(Object.freeze([]), carrierResult, 'Take') + return Object.freeze({ + ...builtin({ + actor: 'Os', + name: options.name, + operation: options.operation, + typeParameters: Object.freeze(['R']), + semanticTypeParameters: Object.freeze([carrierResult]), + parameters: Object.freeze([ + ...options.parameters, + valueParameter('success', 'once fn(OsHandle) -> R'), + valueParameter('failure', 'once fn() -> R'), + ]), + semanticParameters: Object.freeze([...options.semanticParameters, success, failure]), + result: 'Effect', + semanticResult: osEffect(carrierResult), + unsafe: true, + targets: nativeTargets, + }), + invariant: options.invariant, + }) +} + const scalarOperation = (scalar: Scalar.Scalar, operation: Scalar.Operation): Operation => { let concreteResult: Type.Type switch (operation.result) { @@ -844,7 +880,7 @@ const intrinsicOperations = Object.freeze([ invariant: 'true means the complete initialized output contains fresh cryptographically secure bytes; false exposes no recoverable output', }), - osBuiltin({ + osOpen({ name: 'fileOpen', operation: 'OsFileOpen', parameters: Object.freeze([ @@ -855,10 +891,8 @@ const intrinsicOperations = Object.freeze([ valueParameter('nativeCode', '&mut u32'), ]), semanticParameters: Object.freeze([byteSlice, byteSlice, 'i32', mutableI32, mutableU32]), - result: 'Effect>', - semanticResult: Type.option(Type.osHandle), invariant: - 'root is an absolute native path; path is normalized provider-absolute; outputs are initialized; traversal rejects symlinks and namespace escape', + 'root is an absolute native path; path is normalized provider-absolute; status outputs are initialized; traversal rejects symlinks and namespace escape; success transfers one live handle only to the selected carrier', }), osBuiltin({ name: 'fileRead', @@ -906,7 +940,7 @@ const intrinsicOperations = Object.freeze([ invariant: 'handle is a live file; input is initialized; success reports the exact transferred byte count and may be partial', }), - osBuiltin({ + osOpen({ name: 'directoryOpen', operation: 'OsDirectoryOpen', parameters: Object.freeze([ @@ -916,9 +950,8 @@ const intrinsicOperations = Object.freeze([ valueParameter('nativeCode', '&mut u32'), ]), semanticParameters: Object.freeze([byteSlice, byteSlice, mutableI32, mutableU32]), - result: 'Effect>', - semanticResult: Type.option(Type.osHandle), - invariant: 'root and path satisfy confined traversal and outputs are initialized', + invariant: + 'root and path satisfy confined traversal; status outputs are initialized; success transfers one live handle only to the selected carrier', }), osBuiltin({ name: 'directoryNext', diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 889d2388f..41e815869 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -1182,6 +1182,45 @@ export function lowerExpressionInner( } const type = fn.type(expression.type) if (type === undefined) return undefined + if (recipe.operation === 'OsFileOpen' || recipe.operation === 'OsDirectoryOpen') { + const success = arguments_.at(-2) + const failure = arguments_.at(-1) + const successType = + success === undefined ? undefined : fn.localTypes.at(success.ordinal) + const failureType = + failure === undefined ? undefined : fn.localTypes.at(failure.ordinal) + const handleType = fn.type(Type.osHandle) + if ( + success === undefined || + failure === undefined || + successType?._tag !== 'CallableValue' || + failureType?._tag !== 'CallableValue' || + handleType?._tag !== 'Nominal' + ) + return undefined + const valid = fn.alloc(bool) + const handle = fn.alloc(handleType) + const destination = fn.alloc(type) + fn.emit( + Object.freeze({ + _tag: 'OsOpen' as const, + operation: recipe.intrinsic, + destination, + valid, + handle, + arguments: Object.freeze(arguments_.slice(0, -2)), + success, + failure, + successCleanup: callableLocalCleanup(fn, successType), + failureCleanup: callableLocalCleanup(fn, failureType), + handleType, + type, + provenance: authored(expression.span), + }), + ) + endLoans(fn, recipe.loanEnds, expression.span) + return Object.freeze({ result: destination }) + } const destination = fn.alloc(type) fn.emit( Object.freeze({ diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index 66457c557..12401d725 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -475,6 +475,22 @@ export type Operation = readonly failureTag: number readonly provenance: Provenance } + | { + /** Opens one affine OS handle and transfers it only through the selected exact carrier. */ + readonly _tag: 'OsOpen' + readonly operation: Intrinsic.OperationId + readonly destination: LocalId + readonly valid: LocalId + readonly handle: LocalId + readonly arguments: ReadonlyArray + readonly success: LocalId + readonly failure: LocalId + readonly successCleanup: CleanupPlan.CleanupPlan + readonly failureCleanup: CleanupPlan.CleanupPlan + readonly handleType: Extract + readonly type: Type + readonly provenance: Provenance + } | { /** Executes one validated native-only opaque-handle protocol operation. */ readonly _tag: 'OsCall' diff --git a/packages/compiler/src/MirEncoding.ts b/packages/compiler/src/MirEncoding.ts index f1cce3cdf..5b8e6d563 100644 --- a/packages/compiler/src/MirEncoding.ts +++ b/packages/compiler/src/MirEncoding.ts @@ -93,6 +93,8 @@ const operationText = (operation: Operation): string => { return `${localText(operation.destination)} = allocate ${localText(operation.layout)} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` case 'HostWrite': return `${localText(operation.destination)} = standard-stream-write destination=${localText(operation.stream)} bytes=${localText(operation.bytes)} failure=${SilkType.encode(operation.failure)} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` + case 'OsOpen': + return `${localText(operation.destination)} = os-open ${operation.operation.actor}.${operation.operation.name}(${operation.arguments.map(localText).join(', ')}) success=${localText(operation.success)} failure=${localText(operation.failure)} : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` case 'OsCall': return `${localText(operation.destination)} = os-call ${operation.operation.actor}.${operation.operation.name}(${operation.arguments.map(localText).join(', ')}) : ${typeText(operation.type)} ${provenanceText(operation.provenance)}` case 'RawBufferFrom': diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index 0127bd73f..374e4932c 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -44,6 +44,7 @@ export type LinearOperation = | 'Conditional' | 'ShortCircuit' | 'CheckedScalar' + | 'OsOpen' | 'PropagateEffectFailure' } > @@ -57,6 +58,15 @@ export type LinearOperation = readonly valueType: Mir.ScalarType readonly provenance: Mir.Provenance } + | { + readonly _tag: 'OsOpenOutcome' + readonly operation: Extract['operation'] + readonly valid: Mir.LocalId + readonly handle: Mir.LocalId + readonly arguments: ReadonlyArray + readonly handleType: Extract + readonly provenance: Mir.Provenance + } | { readonly _tag: 'BindMatch' readonly scrutinee: Mir.LocalId @@ -73,6 +83,7 @@ export const isLinearOperation = ( operation._tag !== 'Conditional' && operation._tag !== 'ShortCircuit' && operation._tag !== 'CheckedScalar' && + operation._tag !== 'OsOpen' && operation._tag !== 'PropagateEffectFailure' export const linearOperations = ( @@ -161,6 +172,8 @@ export const destinationOf = (operation: LinearOperation): Mir.LocalId | undefin return operation.destination case 'CheckedScalarOutcome': return operation.value + case 'OsOpenOutcome': + return operation.valid case 'BindMatch': return operation.binding.destination case 'CheckPlace': @@ -175,6 +188,7 @@ export const opensRuntimeContinuation = (operation: LinearOperation): boolean => operation._tag === 'Allocate' || operation._tag === 'HostWrite' || operation._tag === 'OsCall' || + operation._tag === 'OsOpenOutcome' || operation._tag === 'RawBufferFrom' || operation._tag === 'SharedFromAllocation' || operation._tag === 'ExecutionFromAllocation' || @@ -234,6 +248,7 @@ export const expandMatches = ( operation._tag === 'Conditional' || operation._tag === 'ShortCircuit' || operation._tag === 'CheckedScalar' || + operation._tag === 'OsOpen' || operation._tag === 'PropagateEffectFailure', ) if (specialIndex < 0) { @@ -344,6 +359,88 @@ export const expandMatches = ( ) return } + if (special?._tag === 'OsOpen') { + const successType = fn.localTypes.at(special.success.ordinal) + const failureType = fn.localTypes.at(special.failure.ordinal) + if (successType?._tag !== 'CallableValue' || failureType?._tag !== 'CallableValue') + throw new RangeError('LLVM OS open expansion lost its carrier callables') + const following = reserve() + const succeeded = reserve() + const failed = reserve() + const apply = ( + callable: Mir.LocalId, + callableType: Extract, + arguments_: ReadonlyArray, + ): Extract => + Object.freeze({ + _tag: 'ApplyCallable', + destination: special.destination, + callable, + typeArguments: + callableType.environment?.callable.typeArguments ?? + callableType.storage?.realization.targetArguments ?? + callableType.typeArguments ?? + Object.freeze([]), + captures: Object.freeze([]), + arguments: arguments_, + callableType: callableType.type, + access: callableType.type.mode, + evaluation: 'CalleeThenArguments', + realization: 'Environment', + type: special.type, + provenance: special.provenance, + }) + const drop = ( + local: Mir.LocalId, + cleanup: Extract['cleanup'], + ): Extract => + Object.freeze({ _tag: 'Drop', local, cleanup, provenance: special.provenance }) + blocks.push( + Object.freeze({ + id, + origin, + kind, + operations: linearOperations([ + ...operations.slice(0, specialIndex), + Object.freeze({ + _tag: 'OsOpenOutcome' as const, + operation: special.operation, + valid: special.valid, + handle: special.handle, + arguments: special.arguments, + handleType: special.handleType, + provenance: special.provenance, + }), + ]), + terminator: Object.freeze({ + _tag: 'Branch', + condition: special.valid, + taken: succeeded, + otherwise: failed, + provenance: special.provenance, + }), + }), + ) + lowerSequence(following, origin, kind, operations.slice(specialIndex + 1), terminator) + lowerSequence( + succeeded, + origin, + 'Normal', + [ + drop(special.failure, special.failureCleanup), + apply(special.success, successType, Object.freeze([special.handle])), + ], + jump(following, special.provenance), + ) + lowerSequence( + failed, + origin, + 'Normal', + [drop(special.success, special.successCleanup), apply(special.failure, failureType, [])], + jump(following, special.provenance), + ) + return + } if (special?._tag === 'Conditional') { const following = reserve() const taken = reserve() diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index b03891bef..bc34552a4 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -696,6 +696,15 @@ export const operationLocals = (operation: Operation): ReadonlyArray => return [operation.destination, operation.layout] case 'HostWrite': return [operation.destination, operation.stream, operation.bytes] + case 'OsOpen': + return [ + operation.destination, + operation.valid, + operation.handle, + ...operation.arguments, + operation.success, + operation.failure, + ] case 'OsCall': return [operation.destination, ...operation.arguments] case 'RawBufferFrom': @@ -1558,6 +1567,14 @@ const operationTypes = (operation: Operation): ReadonlyArray => { return [operation.layout] case 'HostWrite': return [operation.stream, operation.bytes] + case 'OsOpen': + return [...operation.arguments, operation.success, operation.failure] case 'OsCall': return operation.arguments case 'RawBufferFrom': @@ -3452,6 +3471,53 @@ export const verify = (self: Module): ReadonlyArray => { ) } } + if (operation._tag === 'OsOpen') { + const catalog = Intrinsic.findOperationById(operation.operation) + const rule = catalog?.rule._tag === 'BuiltinRule' ? catalog.rule : undefined + const destination = fn.localTypes.at(operation.destination.ordinal) + const valid = fn.localTypes.at(operation.valid.ordinal) + const handle = fn.localTypes.at(operation.handle.ordinal) + const success = fn.localTypes.at(operation.success.ordinal) + const failure = fn.localTypes.at(operation.failure.ordinal) + const parameters = rule?.parameters.slice(0, -2) + const argumentsValid = + rule !== undefined && + (rule.operation === 'OsFileOpen' || rule.operation === 'OsDirectoryOpen') && + parameters?.length === operation.arguments.length && + parameters.every((expected, ordinal) => { + const argument = operation.arguments.at(ordinal) + const actual = argument === undefined ? undefined : fn.localTypes.at(argument.ordinal) + return actual !== undefined && SilkType.equals(semanticType(actual), expected) + }) + if ( + catalog?.unsafe !== true || + catalog.targets.includes('Wasm') || + destination === undefined || + valid?._tag !== 'bool' || + handle?._tag !== 'Nominal' || + !SilkType.equals(handle.type, SilkType.osHandle) || + !SilkType.equals(operation.handleType.type, SilkType.osHandle) || + success?._tag !== 'CallableValue' || + success.type.parameters.length !== 1 || + !SilkType.equals(success.type.parameters[0] ?? 'never', SilkType.osHandle) || + !SilkType.equals(success.type.result, semanticType(operation.type)) || + failure?._tag !== 'CallableValue' || + failure.type.parameters.length !== 0 || + !SilkType.equals(failure.type.result, semanticType(operation.type)) || + !SilkType.equals(semanticType(destination), semanticType(operation.type)) || + !argumentsValid + ) { + violations.push( + Object.freeze({ + _tag: 'Violation', + rule: 'InvalidOsOperation', + function: fn.id, + region: region.id, + detail: 'OS open does not match its affine carrier signature', + }), + ) + } + } if (operation._tag === 'OsCall') { const catalog = Intrinsic.findOperationById(operation.operation) const rule = catalog?.rule._tag === 'BuiltinRule' ? catalog.rule : undefined diff --git a/packages/compiler/src/NativeMemoryOperation.ts b/packages/compiler/src/NativeMemoryOperation.ts index c097b65ba..8c951797d 100644 --- a/packages/compiler/src/NativeMemoryOperation.ts +++ b/packages/compiler/src/NativeMemoryOperation.ts @@ -24,6 +24,7 @@ type Operation = Extract< | 'Allocate' | 'HostWrite' | 'OsCall' + | 'OsOpenOutcome' | 'RawBufferFrom' | 'SharedFromAllocation' | 'SharedClone' @@ -243,6 +244,53 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op nativeStorage.locals.set(operation.destination.ordinal, Object.freeze([])) break } + case 'OsOpenOutcome': { + const runtime = osRuntimes.get(operation.operation.name) + if (runtime === undefined || runtime.abi !== 'OpenOut') + throw new RangeError(`LLVM OS open runtime ${operation.operation.name} is unavailable`) + const arguments_ = operation.arguments.flatMap((argument) => [ + ...NativeStorage.readLocal(nativeStorage, argument), + ]) + const handleLanes = NativeType.lanesFor(types, operation.handleType) + const outputs = yield* Effect.forEach(handleLanes, (lane, ordinal) => + Effect.gen(function* () { + const type = NativeType.laneType(types, lane) + const output = yield* FunctionBody.alloca( + body, + type, + `os${operation.valid.ordinal}_out${ordinal}`, + ) + yield* FunctionBody.store(body, yield* Constant.zero(builder, type), output) + return output + }), + ) + const result = yield* FunctionBody.callDirect( + body, + runtime.handle, + [...arguments_, ...outputs], + `os${operation.valid.ordinal}`, + ) + for (const root of [...nativeStorage.addressRoots].sort((left, right) => left - right)) { + yield* NativeStorage.reloadAddressRoot(nativeStorage, root) + } + if (result === undefined) throw new RangeError('LLVM OS open runtime returned no status') + const handle: Array = [] + for (const [ordinal, output] of outputs.entries()) { + const lane = handleLanes.at(ordinal) + if (lane === undefined) throw new RangeError('LLVM OS open runtime lost an output lane') + handle.push( + yield* FunctionBody.load( + body, + NativeType.laneType(types, lane), + output, + `os${operation.valid.ordinal}_out${ordinal}_value`, + ), + ) + } + nativeStorage.locals.set(operation.valid.ordinal, Object.freeze([result])) + nativeStorage.locals.set(operation.handle.ordinal, Object.freeze(handle)) + break + } case 'OsCall': { const runtime = osRuntimes.get(operation.operation.name) if (runtime === undefined) { @@ -251,21 +299,10 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op const arguments_ = operation.arguments.flatMap((argument) => [ ...NativeStorage.readLocal(nativeStorage, argument), ]) - const resultLanes = NativeType.lanesFor(types, operation.type) - const openOutputs = - runtime.abi === 'OpenOut' - ? yield* Effect.forEach(resultLanes.slice(1), (lane, ordinal) => - FunctionBody.alloca( - body, - NativeType.laneType(types, lane), - `os${operation.destination.ordinal}_out${ordinal}`, - ), - ) - : Object.freeze([]) const result = yield* FunctionBody.callDirect( body, runtime.handle, - [...arguments_, ...openOutputs], + arguments_, `os${operation.destination.ordinal}`, ) for (const root of [...nativeStorage.addressRoots].sort((left, right) => left - right)) { @@ -276,23 +313,6 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op break } if (result === undefined) throw new RangeError('LLVM OS runtime returned no value') - if (runtime.abi === 'OpenOut') { - const values: Array = [result] - for (const [ordinal, output] of openOutputs.entries()) { - const lane = resultLanes.at(ordinal + 1) - if (lane === undefined) throw new RangeError('LLVM OS open runtime lost an output lane') - values.push( - yield* FunctionBody.load( - body, - NativeType.laneType(types, lane), - output, - `os${operation.destination.ordinal}_out${ordinal}_value`, - ), - ) - } - nativeStorage.locals.set(operation.destination.ordinal, Object.freeze(values)) - break - } if (runtime.resultLaneCount === 1) { nativeStorage.locals.set(operation.destination.ordinal, Object.freeze([result])) break diff --git a/packages/compiler/src/NativeOperation.ts b/packages/compiler/src/NativeOperation.ts index afd906c30..1da1ec798 100644 --- a/packages/compiler/src/NativeOperation.ts +++ b/packages/compiler/src/NativeOperation.ts @@ -69,6 +69,7 @@ export const emit = Effect.fnUntraced(function* ( case 'Allocate': case 'HostWrite': case 'OsCall': + case 'OsOpenOutcome': case 'RawBufferFrom': case 'SharedFromAllocation': case 'SharedClone': diff --git a/packages/compiler/src/NativeProgram.ts b/packages/compiler/src/NativeProgram.ts index abf9d4617..fdb67ae27 100644 --- a/packages/compiler/src/NativeProgram.ts +++ b/packages/compiler/src/NativeProgram.ts @@ -267,12 +267,15 @@ export const emit = Effect.fn('NativeProgram.emit')(function* ( } >() for (const operation of program.functions.flatMap((fn) => MirVerification.operations(fn))) { - if (operation._tag !== 'OsCall' || osRuntimes.has(operation.operation.name)) continue - const resultLanes = lanesFor(operation.type) - const abi = - operation.operation.name === 'osFileOpen' || operation.operation.name === 'osDirectoryOpen' - ? 'OpenOut' - : 'Direct' + if ( + (operation._tag !== 'OsCall' && operation._tag !== 'OsOpen') || + osRuntimes.has(operation.operation.name) + ) + continue + const resultLanes = lanesFor( + operation._tag === 'OsOpen' ? operation.handleType : operation.type, + ) + const abi = operation._tag === 'OsOpen' ? 'OpenOut' : 'Direct' const singleResultLane = resultLanes.at(0) let resultType: LlvmType.Type if (abi === 'OpenOut') { @@ -301,7 +304,7 @@ export const emit = Effect.fn('NativeProgram.emit')(function* ( yield* LlvmType.functionType( builder, resultType, - abi === 'OpenOut' ? [...parameters, pointer, pointer, pointer] : parameters, + abi === 'OpenOut' ? [...parameters, ...resultLanes.map(() => pointer)] : parameters, ), ), resultLaneCount: resultLanes.length, diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index 82a66ada9..76d657735 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -842,7 +842,7 @@ export const modules = [ module: 'silk/os_filesystem', path: 'silk/os_filesystem.silk', sourceIdentity: 'silk/os_filesystem', - digest: '23a3e3878b10a6c1bda418342569af8b65356c8360df093a01349a844cff5f09', + digest: '7b546eab10afab67c40e02f6b457574d049b1fe7fd7b969dcb96e2418baed174', documentation: 'silk/os_filesystem.silk', layer: 'target-provider', providerTargets: ['Evaluator', 'LLVM'], @@ -861,7 +861,7 @@ export const modules = [ ], namespace: 'OsFileSystem', source: - '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.effect { Effect }\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let mut received = false\n unsafe {\n received = run Intrinsic.osFileRead(\n handle,\n output,\n &mut length,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if received == false { return run raise(readFileOperation(), lowReason, nativeCode) }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Effect.result(readLoop(&mut handle))\n let closed = run Effect.result(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut length = usize.ZERO\n let mut written = false\n unsafe {\n written = run Intrinsic.osFileWrite(\n handle,\n bytes,\n offset,\n &mut length,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if written == false { return run raise(writeFileOperation(), lowReason, nativeCode) }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Effect.result(writeLoop(&mut handle, bytes))\n let closed = run Effect.result(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let mut next = false\n unsafe {\n next = run Intrinsic.osDirectoryNext(\n handle,\n output,\n &mut length,\n &mut kind,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if next == false {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Effect.result(listLoop(&mut handle, path))\n let closed = run Effect.result(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n count: &mut usize,\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(\n root,\n parent,\n prefix,\n output,\n count,\n required,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut length,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n if chosen == false {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', + '//! Native [`FileSystem`] provider confined beneath one explicitly owned platform root.\n//!\n//! # When to use\n//! Construct [`OsFileSystem`] at a native application edge and provide it to portable filesystem\n//! code. Supply an in-memory [`FileSystem`] in tests or on targets without native OS access.\n//!\n//! # Details\n//! Portable `/` denotes the provider root rather than the host filesystem root. The native boundary\n//! rejects malformed paths, root escape, and symlink traversal outside that confinement. Whole-file\n//! reads and writes own or commit complete contents, directory listings retry oversized entries and\n//! sort complete child paths deterministically. Low-level failures become portable [`FileError`]\n//! values with retained native codes.\n//!\n//! [`make`] copies its root. The root must be an absolute, non-empty, NUL-free native path. A root\n//! that violates this precondition traps. Open handles close on success and failure. If an\n//! operation and close both fail, the operation\'s original typed failure remains the reported\n//! result.\n//!\n//! Constructing the provider performs no filesystem operation beyond owning the root bytes.\n//! Portable code uses `FileSystem` operations after the application supplies `&mut OsFileSystem`\n//! for the `&mut FileSystem` requirement.\n//!\n//! # Gotchas\n//! Reachable OS filesystem operations are native-only. Direct WebAssembly compilation rejects them\n//! rather than inventing filesystem imports; evaluator execution requires an injected adapter.\n//!\n//! # Examples\n//! ## Construct a provider without accessing the filesystem\n//!\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.os_filesystem as OsFileSystem\n//!\n//! effect fn program() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let provider = run OsFileSystem.make("/tmp")\n//! |> Effect.provideMut(&mut allocator)\n//! drop provider\n//! return 42\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(program(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.effect { Effect }\nimport silk.bytes {\n Bytes,\n append as bytesAppend,\n asMutSlice as bytesMutSlice,\n asSlice as bytesSlice,\n copy as bytesCopy,\n make as bytesMake,\n zeroed as bytesZeroed\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.filesystem {\n DirectoryEntry,\n DirectoryInfo,\n FileError,\n FileInfo,\n FileOperation,\n FileReason,\n FileSystem,\n Path,\n alreadyExists,\n createDirectoryOperation,\n createTemporaryDirectoryOperation,\n directory,\n directoryEntry,\n directoryInfo,\n errorWithCode,\n file,\n fileInfo,\n invalidPath,\n listDirectoryOperation,\n noSpace,\n notEmpty,\n notFound,\n other,\n permissionDenied,\n readFileOperation,\n removeDirectoryOperation,\n removeFileOperation,\n statOperation,\n tooLarge,\n unsupported,\n writeFileOperation,\n wrongType,\n joinUtf8 as pathJoinUtf8,\n view as pathView\n}\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string { utf8Bytes as stringUtf8Bytes }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n asSlice as vectorSlice,\n insert as vectorInsert,\n make as vectorMake\n}\n\n/// A native [`FileSystem`] provider confined beneath one independently owned platform root.\n///\n/// # Details\n///\n/// Portable absolute paths resolve inside this root. The provider never exposes the root as a\n/// [`Path`], and operations reject lexical or symbolic-link escape from the root.\npub struct OsFileSystem {\n root: Bytes\n}\n\n/// Copies one absolute native root and creates a confined filesystem provider.\n///\n/// # When to use\n///\n/// Use this function at a native application edge. Provide the result as `&mut FileSystem` to code\n/// that uses the portable filesystem service.\n///\n/// # Details\n///\n/// Construction owns the root bytes but does not open the directory. Portable `/` then denotes\n/// this provider root instead of the host filesystem root.\n///\n/// # Gotchas\n///\n/// `root` must be non-empty, absolute, and NUL-free. A value that violates this precondition traps.\n/// Allocation failure leaves no provider value.\npub effect fn make(root: string) -> OsFileSystem ! OutOfMemoryError ? &mut Allocator {\n let rootBytes = stringUtf8Bytes(root)\n if rootBytes.length == usize.ZERO { let invalid = 1 / 0 }\n if rootBytes[usize.ZERO] != u8.toU8(47) { let invalid = 1 / 0 }\n let mut index = usize.ZERO\n while index < rootBytes.length {\n if rootBytes[index] == u8.toU8(0) { let invalid = 1 / 0 }\n index = index + usize.ONE\n }\n let owned = run bytesCopy(rootBytes)\n return OsFileSystem { root: move owned }\n}\n\nfn pathBytes(path: &Path) -> &[u8] {\n return stringUtf8Bytes(pathView(path))\n}\n\nfn reason(value: i32) -> FileReason {\n if value == 0 { return notFound() }\n if value == 1 { return alreadyExists() }\n if value == 2 { return permissionDenied() }\n if value == 3 { return invalidPath() }\n if value == 4 { return wrongType() }\n if value == 5 { return notEmpty() }\n if value == 6 { return noSpace() }\n if value == 7 { return tooLarge() }\n if value == 9 { return unsupported() }\n return other()\n}\n\neffect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> never ! FileError {\n fail errorWithCode(move operation, reason(lowReason), u32.toI32(nativeCode))\n}\n\neffect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osFileOpen>(root, path, mode, lowReason, nativeCode, some, none) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option {\n unsafe { return run Intrinsic.osDirectoryOpen>(root, path, lowReason, nativeCode, some, none) }\n let impossible = 1 / 0\n return none()\n}\n\neffect fn rawClose(handle: OsHandle, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osHandleClose(move handle, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawInspect(root: &[u8], path: &[u8], kind: &mut i32, byteLength: &mut usize, lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osPathInspect(root, path, kind, byteLength, lowReason, nativeCode) }\n return false\n}\n\neffect fn rawCreate(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryCreate(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveFile(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osFileRemove(root, path, lowReason, nativeCode) }\n return false\n}\neffect fn rawRemoveDirectory(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> bool {\n unsafe { return run Intrinsic.osDirectoryRemove(root, path, lowReason, nativeCode) }\n return false\n}\n\neffect fn openFile(\n self: &mut OsFileSystem,\n path: &Path,\n mode: i32,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawFileOpen(bytesSlice(&self.root), pathBytes(path), mode, &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn openDirectory(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation\n) -> OsHandle ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let opened = run rawDirectoryOpen(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n return match move opened {\n Option.Some { value: handle } => move handle\n Option.None => run raise(move operation, lowReason, nativeCode)\n }\n}\n\neffect fn close(handle: OsHandle, operation: FileOperation) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let closed = run rawClose(move handle, &mut lowReason, &mut nativeCode)\n if closed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\nfn ignoreClose(result: Result<(), FileError>) -> () {\n return match move result {\n Result<(), FileError>.Success { value: completed } => ()\n Result<(), FileError>.Failure { error: failure } => ()\n }\n}\n\neffect fn rerouteFile(error: FileError) -> never ! FileError { fail move error }\neffect fn rerouteOutOfMemory(error: OutOfMemoryError) -> never ! OutOfMemoryError { fail move error }\n\neffect fn discardThenReroute(value: T, error: FileError) -> never ! FileError {\n drop value\n fail move error\n}\n\neffect fn preserveFile(error: FileError, closed: Result<(), FileError>) -> never ! FileError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn preserveOutOfMemory(\n error: OutOfMemoryError,\n closed: Result<(), FileError>\n) -> never ! OutOfMemoryError {\n let ignored = ignoreClose(move closed)\n fail move error\n}\n\neffect fn readLoop(handle: &mut OsHandle) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut result = bytesMake()\n let mut buffer = run bytesZeroed(256)\n let mut complete = false\n while complete == false {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let mut received = false\n unsafe {\n received = run Intrinsic.osFileRead(\n handle,\n output,\n &mut length,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if received == false { return run raise(readFileOperation(), lowReason, nativeCode) }\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n }\n }\n return move result\n}\n\neffect fn readFile(\n self: &mut OsFileSystem,\n path: &Path\n) -> Bytes ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openFile(move self, path, 0, readFileOperation())\n let attempted = run Effect.result(readLoop(&mut handle))\n let closed = run Effect.result(close(move handle, readFileOperation()))\n return match move attempted {\n Result.Success { value: bytes } => match move closed {\n Result<(), FileError>.Success { value: completed } => move bytes\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute(move bytes, move closeFailure)\n }\n Result.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn writeLoop(handle: &mut OsHandle, bytes: &[u8]) -> () ! FileError {\n let mut offset = usize.ZERO\n while offset < bytes.length {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n // The low-level operation may commit a prefix. Re-presenting the remaining bytes is provider policy.\n let mut length = usize.ZERO\n let mut written = false\n unsafe {\n written = run Intrinsic.osFileWrite(\n handle,\n bytes,\n offset,\n &mut length,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if written == false { return run raise(writeFileOperation(), lowReason, nativeCode) }\n if length == usize.ZERO { return run raise(writeFileOperation(), 10, u32.toU32(0)) }\n offset = offset + length\n }\n return ()\n}\n\neffect fn writeFile(self: &mut OsFileSystem, path: &Path, bytes: &[u8]) -> () ! FileError {\n let mut handle = run openFile(move self, path, 1, writeFileOperation())\n let attempted = run Effect.result(writeLoop(&mut handle, bytes))\n let closed = run Effect.result(close(move handle, writeFileOperation()))\n return match move attempted {\n Result<(), FileError>.Success { value: completed } => match move closed {\n Result<(), FileError>.Success { value: closedValue } => ()\n Result<(), FileError>.Failure { error: closeFailure } => run rerouteFile(move closeFailure)\n }\n Result<(), FileError>.Failure { error: primary } => run preserveFile(move primary, move closed)\n }\n}\n\neffect fn stat(self: &mut OsFileSystem, path: &Path) -> FileInfo | DirectoryInfo ! FileError {\n let mut kind = 0\n let mut byteLength = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let inspected = run rawInspect(bytesSlice(&self.root), pathBytes(path), &mut kind, &mut byteLength, &mut lowReason, &mut nativeCode)\n if inspected == false { return run raise(statOperation(), lowReason, nativeCode) }\n if kind == 0 { return fileInfo(byteLength) }\n return directoryInfo()\n}\n\neffect fn listLoop(\n handle: &mut OsHandle,\n parent: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut entries = vectorMake()\n let initialBuffer = bytesZeroed(64)\n let mut buffer = run initialBuffer\n let mut complete = false\n while complete == false {\n let mut kind = 0\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let mut next = false\n unsafe {\n next = run Intrinsic.osDirectoryNext(\n handle,\n output,\n &mut length,\n &mut kind,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n }\n if next == false {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(listDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n if length == usize.ZERO {\n complete = true\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n let mut entryKind = directory()\n if kind == 0 { entryKind = file() }\n let insertion = insertionFor(&child, vectorSlice(&entries))\n let inserted = run vectorInsert(\n &mut entries,\n insertion,\n directoryEntry(move child, move entryKind)\n )\n }\n }\n }\n return move entries\n}\n\nfn insertionFor(child: &Path, existing: &[DirectoryEntry]) -> usize {\n let mut insertion = usize.ZERO\n while insertion < existing.length {\n let before = match &existing[insertion] {\n DirectoryEntry { path, kind } => pathLess(child, &path)\n }\n if before { return insertion }\n insertion = insertion + usize.ONE\n }\n return insertion\n}\n\nfn pathLess(left: &Path, right: &Path) -> bool {\n let leftBytes = pathBytes(left)\n let rightBytes = pathBytes(right)\n let mut index = usize.ZERO\n while index < leftBytes.length {\n if rightBytes.length <= index { return false }\n if leftBytes[index] < rightBytes[index] { return true }\n if rightBytes[index] < leftBytes[index] { return false }\n index = index + usize.ONE\n }\n if leftBytes.length < rightBytes.length { return true }\n return false\n}\n\neffect fn listDirectory(\n self: &mut OsFileSystem,\n path: &Path\n) -> Vector ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut handle = run openDirectory(move self, path, listDirectoryOperation())\n let attempted = run Effect.result(listLoop(&mut handle, path))\n let closed = run Effect.result(close(move handle, listDirectoryOperation()))\n return match move attempted {\n Result, FileError | OutOfMemoryError>.Success { value: entries } => match move closed {\n Result<(), FileError>.Success { value: completed } => move entries\n Result<(), FileError>.Failure { error: closeFailure } => run discardThenReroute>(move entries, move closeFailure)\n }\n Result, FileError | OutOfMemoryError>.Failure { error: primary } => match move primary {\n FileError failure => run preserveFile(move failure, move closed)\n OutOfMemoryError exhausted => run preserveOutOfMemory(move exhausted, move closed)\n }\n }\n}\n\neffect fn command(\n self: &mut OsFileSystem,\n path: &Path,\n operation: FileOperation,\n selector: i32\n) -> () ! FileError {\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut completed = false\n if selector == 0 {\n completed = run rawCreate(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n if selector == 1 {\n completed = run rawRemoveFile(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n } else {\n completed = run rawRemoveDirectory(bytesSlice(&self.root), pathBytes(path), &mut lowReason, &mut nativeCode)\n }\n }\n if completed == false { return run raise(move operation, lowReason, nativeCode) }\n return ()\n}\n\neffect fn rawCreateUnique(\n root: &[u8],\n parent: &[u8],\n prefix: &[u8],\n output: &mut [u8],\n count: &mut usize,\n required: &mut usize,\n lowReason: &mut i32,\n nativeCode: &mut u32\n) -> bool {\n unsafe {\n return run Intrinsic.osDirectoryCreateUnique(\n root,\n parent,\n prefix,\n output,\n count,\n required,\n lowReason,\n nativeCode\n )\n }\n return false\n}\n\n/// Creates one uniquely named directory under `parent` and returns its complete Path.\n///\n/// The provider chooses the name\'s unique part, so the created name comes back rather than going\n/// in. A buffer too small for that name creates nothing and reports the capacity it needs, which\n/// is why the retry below is safe to take.\neffect fn createTemporaryDirectory(\n self: &mut OsFileSystem,\n parent: &Path,\n prefix: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let mut buffer = run bytesZeroed(64)\n let mut created = none()\n let mut complete = false\n while complete == false {\n let mut required = usize.ZERO\n let mut lowReason = 0\n let mut nativeCode = u32.toU32(0)\n let mut output = bytesMutSlice(&mut buffer)\n let mut length = usize.ZERO\n let chosen = run rawCreateUnique(\n bytesSlice(&self.root),\n pathBytes(parent),\n prefix,\n move output,\n &mut length,\n &mut required,\n &mut lowReason,\n &mut nativeCode\n )\n if chosen == false {\n if lowReason == 8 {\n let resized = bytesZeroed(required)\n buffer = run resized\n } else {\n return run raise(createTemporaryDirectoryOperation(), lowReason, nativeCode)\n }\n } else {\n let view = bytesSlice(&buffer)\n let mut name = bytesMake()\n let mut index = usize.ZERO\n while index < length {\n let one = [view[index]]\n let appended = run bytesAppend(&mut name, &one)\n index = index + usize.ONE\n }\n let child = run pathJoinUtf8(parent, bytesSlice(&name))\n created = some(move child)\n complete = true\n }\n }\n return match move created {\n Option.Some { value: path } => move path\n Option.None => run raise(createTemporaryDirectoryOperation(), 10, u32.toU32(0))\n }\n}\n\neffect fn createDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, createDirectoryOperation(), 0)\n}\neffect fn removeFile(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeFileOperation(), 1)\n}\neffect fn removeDirectory(self: &mut OsFileSystem, path: &Path) -> () ! FileError {\n return run command(move self, path, removeDirectoryOperation(), 2)\n}\n\nimpl FileSystem for OsFileSystem {\n readFile: OsFileSystem.readFile\n writeFile: OsFileSystem.writeFile\n stat: OsFileSystem.stat\n listDirectory: OsFileSystem.listDirectory\n createDirectory: OsFileSystem.createDirectory\n removeFile: OsFileSystem.removeFile\n removeDirectory: OsFileSystem.removeDirectory\n createTemporaryDirectory: OsFileSystem.createTemporaryDirectory\n}\n', }, { module: 'silk/os_host_input', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index ffb34ac94..6955e3348 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'f9b5ea29e7049585ec1eb8083531948bae405ee950125e5c4298a0aa6ab5dd5e' +export const compilerDigest = 'e7b8173a01f2114bff556737f27b35162b0d584948ad3b94a4ee7e51041a8e52' diff --git a/packages/compiler/src/Type.ts b/packages/compiler/src/Type.ts index 9719f3a48..68d79a198 100644 --- a/packages/compiler/src/Type.ts +++ b/packages/compiler/src/Type.ts @@ -527,10 +527,6 @@ export const execution = (result: Type): Nominal => sealedExecution([result]) export const wake: Nominal = sealedWake() /** Sealed host-storage refusal carried only by the primitive allocation boundary. */ export const storageFailure: Nominal = sealedStorageFailure() -/** Canonical recoverable success and failure members shipped by silk/option. */ -export const some = (element: Type): Nominal => nominal('silk/option', 'Some', [element]) -export const none: Nominal = nominal('silk/option', 'None') - /** Normalizes one or more ordinary failure types to their runtime value union. */ export const failureValue = (failures: ReadonlyArray): Type => { const only = failures.at(0) @@ -539,12 +535,6 @@ export const failureValue = (failures: ReadonlyArray): Type => { return normalized._tag === 'Normalized' ? normalized.type : 'never' } -/** Canonical transparent Option identity, represented as the ordinary structural union. */ -export const option = (element: Type): Type => { - const normalized = union([some(element), none]) - return normalized._tag === 'Normalized' ? normalized.type : 'never' -} - export const isRawBuffer = ( self: Type, ): self is Nominal & { @@ -2215,26 +2205,6 @@ export const encode = (self: Type): string => { return `${access}Effect<${encode(self.success)}${row}${requirements}>` } if (isRepresented(self)) return encode(self.contract) - const someMember = self.members.find( - (member): member is Nominal => - isNominal(member) && - member.module === 'silk/option' && - member.name === 'Some' && - member.arguments.length === 1, - ) - const noneMember = self.members.find( - (member): member is Nominal => - isNominal(member) && member.module === 'silk/option' && member.name === 'None', - ) - const someArgument = someMember?.arguments.at(0) - if ( - self.members.length === 2 && - someMember !== undefined && - noneMember !== undefined && - someArgument !== undefined && - isTypeArgument(someArgument) - ) - return `Option<${encode(someArgument)}>` return self.members.map(encode).join(' | ') } diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index fa11d9c96..74433e0ce 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -3396,7 +3396,7 @@ const emitHostWriteOperation = ( } const emitOsCallOperation = ( - _operation: Extract, + _operation: Extract, _state: WasmOperationContext, ): ReadonlyArray => { throw new RangeError('Target validation allowed a native-only OS operation into Wasm') @@ -7289,6 +7289,7 @@ const emitOperationWithContext = ( return emitAllocateOperation(operation, context) case 'HostWrite': return emitHostWriteOperation(operation, context) + case 'OsOpen': case 'OsCall': return emitOsCallOperation(operation, context) case 'RawBufferFrom': diff --git a/packages/compiler/stdlib/silk/os_filesystem.silk b/packages/compiler/stdlib/silk/os_filesystem.silk index 0dc8eaa17..711b9fb00 100644 --- a/packages/compiler/stdlib/silk/os_filesystem.silk +++ b/packages/compiler/stdlib/silk/os_filesystem.silk @@ -176,13 +176,13 @@ effect fn raise(operation: FileOperation, lowReason: i32, nativeCode: u32) -> ne } effect fn rawFileOpen(root: &[u8], path: &[u8], mode: i32, lowReason: &mut i32, nativeCode: &mut u32) -> Option { - unsafe { return run Intrinsic.osFileOpen(root, path, mode, lowReason, nativeCode) } + unsafe { return run Intrinsic.osFileOpen>(root, path, mode, lowReason, nativeCode, some, none) } let impossible = 1 / 0 return none() } effect fn rawDirectoryOpen(root: &[u8], path: &[u8], lowReason: &mut i32, nativeCode: &mut u32) -> Option { - unsafe { return run Intrinsic.osDirectoryOpen(root, path, lowReason, nativeCode) } + unsafe { return run Intrinsic.osDirectoryOpen>(root, path, lowReason, nativeCode, some, none) } let impossible = 1 / 0 return none() } diff --git a/packages/compiler/test/IntegerScalars.test.ts b/packages/compiler/test/IntegerScalars.test.ts index 5cca62340..7d3ecaee7 100644 --- a/packages/compiler/test/IntegerScalars.test.ts +++ b/packages/compiler/test/IntegerScalars.test.ts @@ -134,6 +134,75 @@ it.effect('lets checked scalar intrinsics choose a generic nominal carrier', () }), ) +const affineCheckedCarrier = `import silk.u8 as u8 + +union Checked { + Present { value: T }, + Absent +} + +struct Token { marker: i32 } + +fn present(value: u8, token: Token) -> Checked { + let observed = token.marker + return Checked.Present { value: value } +} + +fn absent() -> Checked { + return Checked.Absent +} + +fn presentWith(token: Token) -> some Checked> F { + return present(move token) +} + +fn value(self: Checked) -> i32 { + return match move self { + Checked.Present { value } => u8.toI32(value) + Checked.Absent => 0 + } +} + +pub fn main() -> i32 { + let firstPresent = Token { marker: 1 } + let secondPresent = Token { marker: 2 } + let succeeded = Intrinsic.u8CheckedAdd>( + u8.toU8(40), + u8.toU8(2), + presentWith(move firstPresent), + absent + ) + let failed = Intrinsic.u8CheckedAdd>( + u8.toU8(255), + u8.toU8(1), + presentWith(move secondPresent), + absent + ) + return value(move succeeded) + value(move failed) +}` + +it.effect('cleans the unused affine carrier and invokes the selected carrier exactly once', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'integer/affine-checked-carrier', + new TextEncoder().encode(affineCheckedCarrier), + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual( + evaluated._tag, + 'Completed', + JSON.stringify(evaluated, (_, value) => (typeof value === 'bigint' ? `${value}n` : value), 2), + ) + if (evaluated._tag === 'Completed') assert.strictEqual(evaluated.result.value, 42n) + assert.strictEqual(evaluated.trace.filter((event) => event._tag === 'CallableApply').length, 2) + assert.strictEqual( + evaluated.trace.filter((event) => event._tag === 'CallableCleanup').length, + 1, + ) + }), +) + const characters = `import silk.u32 as u32 import silk.char { fromU32, toU32 } import silk.option { Option } diff --git a/packages/compiler/test/IntrinsicCatalog.test.ts b/packages/compiler/test/IntrinsicCatalog.test.ts index 1556858b1..4f1cfd420 100644 --- a/packages/compiler/test/IntrinsicCatalog.test.ts +++ b/packages/compiler/test/IntrinsicCatalog.test.ts @@ -242,8 +242,10 @@ pub effect fn main() -> () ! StreamWriteError { return () }`, `import silk.usize as usize -import silk.option { Option, none } +import silk.option { Option, none, some } fn absurd() -> T { let boom = 1 / 0 return absurd() } +fn opened(handle: OsHandle) -> Option { return some(move handle) } +fn refused() -> Option { return none() } effect fn systemClockNow(seconds: &mut i64, nanoseconds: &mut i64) -> bool { unsafe { return run Intrinsic.osSystemClockNow(seconds, nanoseconds) } return false @@ -269,7 +271,7 @@ effect fn randomFill(output: &mut [u8]) -> bool { return false } effect fn fileOpen(root: &[u8], path: &[u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osFileOpen(root, path, 0, reason, code) } + unsafe { return run Intrinsic.osFileOpen>(root, path, 0, reason, code, opened, refused) } return none() } effect fn fileRead(handle: &mut OsHandle, output: &mut [u8], count: &mut usize, reason: &mut i32, code: &mut u32) -> bool { @@ -281,7 +283,7 @@ effect fn fileWrite(handle: &mut OsHandle, input: &[u8], count: &mut usize, reas return false } effect fn directoryOpen(root: &[u8], path: &[u8], reason: &mut i32, code: &mut u32) -> Option { - unsafe { return run Intrinsic.osDirectoryOpen(root, path, reason, code) } + unsafe { return run Intrinsic.osDirectoryOpen>(root, path, reason, code, opened, refused) } return none() } effect fn directoryNext(handle: &mut OsHandle, output: &mut [u8], count: &mut usize, kind: &mut i32, required: &mut usize, reason: &mut i32, code: &mut u32) -> bool { diff --git a/packages/compiler/test/OsFileSystem.test.ts b/packages/compiler/test/OsFileSystem.test.ts index f3ef246e3..62ebccd3a 100644 --- a/packages/compiler/test/OsFileSystem.test.ts +++ b/packages/compiler/test/OsFileSystem.test.ts @@ -142,6 +142,86 @@ it.effect('blocks OS evaluation without an injected adapter and uses one when su }), ) +it.effect('selects an affine OS open carrier without constructing Option in the compiler', () => + Effect.gen(function* () { + const source = `import silk.u32 as u32 + +union OpenAttempt { + Opened { handle: OsHandle }, + Refused +} + +fn opened(handle: OsHandle) -> OpenAttempt { + return OpenAttempt.Opened { handle: move handle } +} + +fn refused() -> OpenAttempt { return OpenAttempt.Refused } + +effect fn rawOpen(reason: &mut i32, nativeCode: &mut u32) -> OpenAttempt { + unsafe { + return run Intrinsic.osFileOpen( + Intrinsic.stringUtf8Bytes("/root"), + Intrinsic.stringUtf8Bytes("/file"), + 0, + reason, + nativeCode, + opened, + refused + ) + } + let impossible = 1 / 0 + return refused() +} + +effect fn rawClose(handle: OsHandle, reason: &mut i32, nativeCode: &mut u32) -> bool { + unsafe { return run Intrinsic.osHandleClose(move handle, reason, nativeCode) } + return false +} + +effect fn finish(handle: OsHandle, reason: &mut i32, nativeCode: &mut u32) -> i32 { + let closed = run rawClose(move handle, move reason, move nativeCode) + if closed { return 42 } + return 2 +} + +effect fn program() -> i32 { + let mut reason = 0 + let mut nativeCode = u32.toU32(0) + let attempt = run rawOpen(&mut reason, &mut nativeCode) + return match move attempt { + OpenAttempt.Opened { handle } => run finish(move handle, &mut reason, &mut nativeCode) + OpenAttempt.Refused => 10 + reason + } +} + +pub fn main() -> i32 { + return run program() +}` + const snapshot = yield* Analysis.ofSourceRealized('os-filesystem/open-carrier', ascii(source)) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + + let closeCount = 0 + const successful = Analysis.evaluate(snapshot, { + osFileSystem: { + ...provider, + fileOpen: () => ({ _tag: 'Opened', handle: { identity: 7, kind: 'File' } }), + handleClose: () => { + closeCount += 1 + return completed + }, + }, + }) + assert.strictEqual(successful._tag, 'Completed') + if (successful._tag === 'Completed') assert.strictEqual(successful.result.value, 42n) + assert.strictEqual(closeCount, 1) + + const failed = Analysis.evaluate(snapshot, { osFileSystem: provider }) + assert.strictEqual(failed._tag, 'Completed') + if (failed._tag === 'Completed') assert.strictEqual(failed.result.value, 19n) + assert.strictEqual(closeCount, 1) + }), +) + it.effect('preserves an arbitrary thrown OS-provider cause in the evaluation trace', () => Effect.gen(function* () { const snapshot = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/fixtures/intrinsic-inventory.json b/packages/compiler/test/fixtures/intrinsic-inventory.json index 52248116e..6e01191a6 100644 --- a/packages/compiler/test/fixtures/intrinsic-inventory.json +++ b/packages/compiler/test/fixtures/intrinsic-inventory.json @@ -5038,12 +5038,12 @@ }, { "operation": "Intrinsic.osFileOpen", - "signature": "unsafe fn Intrinsic.osFileOpen(root: &[u8], path: &[u8], mode: i32, reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osFileOpen(root: &[u8], path: &[u8], mode: i32, reason: &mut i32, nativeCode: &mut u32, success: once fn(OsHandle) -> R, failure: once fn() -> R) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_filesystem.fileOpen", "identity": "OsFileOpen", - "invariant": "root is an absolute native path; path is normalized provider-absolute; outputs are initialized; traversal rejects symlinks and namespace escape" + "invariant": "root is an absolute native path; path is normalized provider-absolute; status outputs are initialized; traversal rejects symlinks and namespace escape; success transfers one live handle only to the selected carrier" }, { "operation": "Intrinsic.osFileRead", @@ -5065,12 +5065,12 @@ }, { "operation": "Intrinsic.osDirectoryOpen", - "signature": "unsafe fn Intrinsic.osDirectoryOpen(root: &[u8], path: &[u8], reason: &mut i32, nativeCode: &mut u32) -> Effect>", + "signature": "unsafe fn Intrinsic.osDirectoryOpen(root: &[u8], path: &[u8], reason: &mut i32, nativeCode: &mut u32, success: once fn(OsHandle) -> R, failure: once fn() -> R) -> Effect", "unsafe": true, "admission": "Platform", "consumer": "silk/os_filesystem.directoryOpen", "identity": "OsDirectoryOpen", - "invariant": "root and path satisfy confined traversal and outputs are initialized" + "invariant": "root and path satisfy confined traversal; status outputs are initialized; success transfers one live handle only to the selected carrier" }, { "operation": "Intrinsic.osDirectoryNext", From 4637c5a4e014f694986f2076fb2185248508f56b Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 18:08:03 -0300 Subject: [PATCH 19/42] feat(compiler): complete nominal union surfaces --- apps/docs/content/language/stdlib/char.md | 2 +- .../content/language/stdlib/child-process.md | 10 +- apps/docs/content/language/stdlib/effect.md | 18 +- .../content/language/stdlib/filesystem.md | 6 +- apps/docs/content/language/stdlib/hash-map.md | 8 +- apps/docs/content/language/stdlib/hash-set.md | 4 +- .../content/language/stdlib/host-input.md | 12 +- apps/docs/content/language/stdlib/i16.md | 30 +-- apps/docs/content/language/stdlib/i32.md | 30 +-- apps/docs/content/language/stdlib/i64.md | 30 +-- apps/docs/content/language/stdlib/i8.md | 30 +-- apps/docs/content/language/stdlib/index.md | 4 +- .../language/stdlib/insecure-random.md | 2 +- apps/docs/content/language/stdlib/isize.md | 30 +-- apps/docs/content/language/stdlib/option.md | 52 +++--- .../content/language/stdlib/os-host-input.md | 2 +- apps/docs/content/language/stdlib/random.md | 2 +- apps/docs/content/language/stdlib/result.md | 72 +++----- apps/docs/content/language/stdlib/string.md | 7 +- apps/docs/content/language/stdlib/u16.md | 30 +-- apps/docs/content/language/stdlib/u32.md | 30 +-- apps/docs/content/language/stdlib/u64.md | 30 +-- apps/docs/content/language/stdlib/u8.md | 30 +-- apps/docs/content/language/stdlib/usize.md | 30 +-- apps/docs/content/language/stdlib/vector.md | 4 +- .../content/reference/effect-contracts.md | 51 +++++ .../reference/expressions-and-operators.md | 6 +- apps/docs/content/reference/style-guide.md | 8 +- .../content/reference/values-and-types.md | 174 +++++++++++++++++- openspec/changes/add-nominal-unions/tasks.md | 42 ++--- packages/compiler/src/ExpressionAnalysis.ts | 25 ++- packages/compiler/src/InstanceDiagnostics.ts | 32 +++- packages/compiler/src/Layout.ts | 92 ++++----- packages/compiler/src/MirVerification.ts | 47 ++++- packages/compiler/src/RepresentationField.ts | 44 ++++- packages/compiler/src/Stdlib.generated.ts | 4 +- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/stdlib/silk/effect.silk | 4 + .../compiler/test/DeclarationIndex.test.ts | 2 +- .../compiler/test/EditorIntelligence.test.ts | 29 ++- packages/compiler/test/Random.test.ts | 6 - .../compiler/test/RepresentationField.test.ts | 30 +++ packages/compiler/test/StructValues.test.ts | 55 ++++++ .../test/fixtures/intrinsic-inventory.json | 6 +- packages/compiler/test/support/corpus.ts | 67 +++++++ packages/docgen/src/Document.ts | 10 +- packages/docgen/src/Model.ts | 16 +- packages/docgen/src/Project.ts | 46 +++++ packages/docgen/test/Model.test.ts | 4 +- packages/docgen/test/Project.test.ts | 23 ++- packages/lsp/src/Document.ts | 112 +++++++++-- packages/lsp/test/Document.test.ts | 75 +++++++- 52 files changed, 1122 insertions(+), 395 deletions(-) diff --git a/apps/docs/content/language/stdlib/char.md b/apps/docs/content/language/stdlib/char.md index 7f96a85d0..7298b37b5 100644 --- a/apps/docs/content/language/stdlib/char.md +++ b/apps/docs/content/language/stdlib/char.md @@ -55,7 +55,7 @@ Public declarations: 8. ## `fromU32` ```silk -pub fn fromU32(value: u32) -> Option +pub fn fromU32(value: u32) -> silk/option.Option ``` Converts an integer to a Unicode scalar. Returns `None` for `0xD800` through `0xDFFF` and values diff --git a/apps/docs/content/language/stdlib/child-process.md b/apps/docs/content/language/stdlib/child-process.md index 48d908396..cd24ea7a8 100644 --- a/apps/docs/content/language/stdlib/child-process.md +++ b/apps/docs/content/language/stdlib/child-process.md @@ -67,8 +67,8 @@ effect fn program() -> i32 |> Effect.provideMut(&mut provider) |> Effect.provideMut(&mut allocator) return match move Process.exitCode(&outcome) { - Option.Some {value} => 35 + value - Option.None {} => 1 + Option.Option.Some {value} => 35 + value + Option.Option.None => 1 } } @@ -297,7 +297,7 @@ Creates a process failure with a provider-defined numeric code for diagnostics. ## `providerCode` ```silk -pub fn providerCode(error: &silk/child_process.ProcessError) -> Option +pub fn providerCode(error: &silk/child_process.ProcessError) -> silk/option.Option ``` Returns the provider-defined numeric code, or `None` when the failure has no such code. @@ -629,7 +629,7 @@ Reports whether a signal terminated the child instead of an exit code. ## `exitCode` ```silk -pub fn exitCode(outcome: &silk/child_process.ProcessOutcome) -> Option +pub fn exitCode(outcome: &silk/child_process.ProcessOutcome) -> silk/option.Option ``` Returns the exit code, or `None` when a signal terminated the child. @@ -639,7 +639,7 @@ Returns the exit code, or `None` when a signal terminated the child. ## `terminatingSignal` ```silk -pub fn terminatingSignal(outcome: &silk/child_process.ProcessOutcome) -> Option +pub fn terminatingSignal(outcome: &silk/child_process.ProcessOutcome) -> silk/option.Option ``` Returns the terminating signal number, or `None` when the child returned an exit code. diff --git a/apps/docs/content/language/stdlib/effect.md b/apps/docs/content/language/stdlib/effect.md index e663332e3..151cb2134 100644 --- a/apps/docs/content/language/stdlib/effect.md +++ b/apps/docs/content/language/stdlib/effect.md @@ -28,7 +28,7 @@ owned provider bindings have distinct borrowing and capture behavior. ## Gotchas -Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass +Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass [`catchAll`](#declaration-73696c6b2f6566666563743a3a6361746368416c6c), [`ensuring`](#declaration-73696c6b2f6566666563743a3a656e737572696e67), and Drop hooks. [`suspend`](#declaration-73696c6b2f6566666563743a3a73757370656e64) crosses the stack-safe execution boundary while preserving all three channels exactly; frame exhaustion is fatal. @@ -240,8 +240,8 @@ Executes `protected` once and converts its success or typed failure into ordinar ### Details -The returned Effect still requires `R`, because reification does not provide services. Its typed -failure row is empty: an `E` becomes [`Failure`](./result.md#declaration-73696c6b2f726573756c743a3a4661696c757265) data instead of propagating. Traps are not typed +The returned Effect still requires `R`, because conversion does not provide services. Its typed +failure row is empty: an `E` becomes `Failure` data instead of propagating. Traps are not typed failures and therefore are not captured. ### Examples @@ -265,14 +265,16 @@ effect fn load() -> i32 pub fn main() -> i32 { let completed = run Effect.result(load()) return match move completed { - Result.Result {value: outcome} => match move outcome { - Result.Success {value} => value - Result.Failure {error} => error.answer - } + Result.Result.Success {value} => value + Result.Result.Failure {error} => error.answer } } ``` +This is ordinary Silk composition: success is mapped into `Result.Success`, then `catchAll` +maps the complete typed failure value into `Result.Failure`. Compound failure unions and +requirements are preserved, and the exact `once fn` adapters transfer affine payloads once. + ## `mapBoth` @@ -518,7 +520,7 @@ Runs a finalizer after the Effect completes, whatever its outcome, and preserves ### Details -The protected Effect is reified into Result data before the finalizer runs, which is what fixes +The protected Effect is converted into Result data before the finalizer runs, which is what fixes the order: a typed failure reaches this body as data rather than as a propagation, so the protected Effect's own frame — and every local it cleans up — is already gone by the time the finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the diff --git a/apps/docs/content/language/stdlib/filesystem.md b/apps/docs/content/language/stdlib/filesystem.md index 71de71f1a..8f0d8ee80 100644 --- a/apps/docs/content/language/stdlib/filesystem.md +++ b/apps/docs/content/language/stdlib/filesystem.md @@ -534,7 +534,7 @@ the fields callers should use for recovery. ## `providerCode` ```silk -pub fn providerCode(error: &silk/filesystem.FileError) -> Option +pub fn providerCode(error: &silk/filesystem.FileError) -> silk/option.Option ``` Borrows an error and returns its provider-specific numeric detail, if one was retained. @@ -688,10 +688,10 @@ fails with the `InvalidPath` reason. Resolution is lexical and never accesses th ## `parent` ```silk -pub effect fn parent(self: &silk/filesystem.Path) -> Option ! OutOfMemoryError ? &mut Allocator +pub effect fn parent(self: &silk/filesystem.Path) -> silk/option.Option ! OutOfMemoryError ? &mut Allocator ``` -Allocates an independently owned parent path, or [`None`](./option.md#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65) when `self` is root. +Allocates an independently owned parent path, or `None` when `self` is root. ### Details diff --git a/apps/docs/content/language/stdlib/hash-map.md b/apps/docs/content/language/stdlib/hash-map.md index f6c73fdb0..54ba64c94 100644 --- a/apps/docs/content/language/stdlib/hash-map.md +++ b/apps/docs/content/language/stdlib/hash-map.md @@ -186,7 +186,7 @@ impl Drop for silk/hash_map.HashMap ## `insert` ```silk -pub effect fn insert(self: &mut silk/hash_map.HashMap, key: K, value: V) -> Option ! OutOfMemoryError ? &mut Allocator +pub effect fn insert(self: &mut silk/hash_map.HashMap, key: K, value: V) -> silk/option.Option ! OutOfMemoryError ? &mut Allocator ``` Inserts one owned key and value, answering with the value an equivalent key already held. @@ -218,7 +218,7 @@ This function consumes the probe key. It does not change the map or move a store ## `indexOf` ```silk -pub fn indexOf(self: &silk/hash_map.HashMap, key: K) -> Option +pub fn indexOf(self: &silk/hash_map.HashMap, key: K) -> silk/option.Option ``` Returns the bucket holding an entry under a key equivalent to one probe key, or an absent value. @@ -234,7 +234,7 @@ This function consumes the probe key. ## `get` ```silk -pub fn get(self: &silk/hash_map.HashMap, key: K) -> Option +pub fn get(self: &silk/hash_map.HashMap, key: K) -> silk/option.Option ``` Returns the value held under a key equivalent to one probe key, or an absent value. @@ -270,7 +270,7 @@ count unchanged. ## `remove` ```silk -pub fn remove(self: &mut silk/hash_map.HashMap, key: K) -> Option +pub fn remove(self: &mut silk/hash_map.HashMap, key: K) -> silk/option.Option ``` Removes the entry under a key equivalent to one probe key and answers with its value. diff --git a/apps/docs/content/language/stdlib/hash-set.md b/apps/docs/content/language/stdlib/hash-set.md index 611a1e1b6..1f5564d97 100644 --- a/apps/docs/content/language/stdlib/hash-set.md +++ b/apps/docs/content/language/stdlib/hash-set.md @@ -214,7 +214,7 @@ This function consumes the probe element. It does not change the set or move a s ## `indexOf` ```silk -pub fn indexOf(self: &silk/hash_set.HashSet, value: T) -> Option +pub fn indexOf(self: &silk/hash_set.HashSet, value: T) -> silk/option.Option ``` Returns the bucket holding an element equivalent to one probe element, or an absent value. @@ -229,7 +229,7 @@ This function consumes the probe element. ## `remove` ```silk -pub fn remove(self: &mut silk/hash_set.HashSet, value: T) -> Option +pub fn remove(self: &mut silk/hash_set.HashSet, value: T) -> silk/option.Option ``` Removes the element equivalent to one probe element and answers with it. diff --git a/apps/docs/content/language/stdlib/host-input.md b/apps/docs/content/language/stdlib/host-input.md index 9497043a1..707808183 100644 --- a/apps/docs/content/language/stdlib/host-input.md +++ b/apps/docs/content/language/stdlib/host-input.md @@ -13,7 +13,7 @@ lossless pass-through. ## Details Arguments include the program name at index zero and retain host order. A missing argument index -or unset variable is [`None`](./option.md#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65), while [`HostInputError`](#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e7075744572726f72) means the provider could not answer. +or unset variable is `None`, while [`HostInputError`](#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e7075744572726f72) means the provider could not answer. Returned [`Bytes`](./bytes.md#declaration-73696c6b2f62797465733a3a4279746573) values are independently owned, so lookup operations also carry explicit [`OutOfMemoryError`](./allocator.md#declaration-73696c6b2f616c6c6f6361746f723a3a4f75744f664d656d6f72794572726f72) and [`Allocator`](./allocator.md#declaration-73696c6b2f616c6c6f6361746f723a3a416c6c6f6361746f72) channels. @@ -152,7 +152,7 @@ A provider that cannot inspect the process arguments fails with `HostInputError` ### Operation `argument` ```silk -effect fn argument(index: usize) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator +effect fn argument(index: usize) -> silk/option.Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator ``` Copies one argument as raw bytes, or returns `None` when `index` is out of range. @@ -167,7 +167,7 @@ produces `HostInputError`; ownership allocation produces `OutOfMemoryError`. ### Operation `variable` ```silk -effect fn variable(name: &[u8]) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator +effect fn variable(name: &[u8]) -> silk/option.Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator ``` Copies one environment value as raw bytes, or returns `None` when `name` is unset. @@ -212,7 +212,7 @@ The count includes the program name at index zero. Provider failure produces ## `argument` ```silk -pub effect fn argument(index: usize) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator +pub effect fn argument(index: usize) -> silk/option.Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator ``` Copies one process argument through the active [`HostInput`](#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e707574) provider. @@ -227,7 +227,7 @@ independently owned and can contain bytes that are not valid UTF-8. ## `variable` ```silk -pub effect fn variable(name: &[u8]) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator +pub effect fn variable(name: &[u8]) -> silk/option.Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator ``` Copies one environment value selected by a raw byte name. @@ -242,7 +242,7 @@ process environment. The returned [`Bytes`](./bytes.md#declaration-73696c6b2f627 ## `variableNamed` ```silk -pub effect fn variableNamed(name: string) -> Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator +pub effect fn variableNamed(name: string) -> silk/option.Option ! HostInputError | OutOfMemoryError ? &mut HostInput | &mut Allocator ``` Copies one environment value selected by a valid UTF-8 name. diff --git a/apps/docs/content/language/stdlib/i16.md b/apps/docs/content/language/stdlib/i16.md index 2b4f1ff0c..683eadb62 100644 --- a/apps/docs/content/language/stdlib/i16.md +++ b/apps/docs/content/language/stdlib/i16.md @@ -118,7 +118,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: i16) -> Option +pub fn checkedToU8(value: i16) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -140,7 +140,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: i16) -> Option +pub fn checkedToU16(value: i16) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -162,7 +162,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: i16) -> Option +pub fn checkedToU32(value: i16) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -184,7 +184,7 @@ this function when an out-of-range value is a program error. ## `checkedToU64` ```silk -pub fn checkedToU64(value: i16) -> Option +pub fn checkedToU64(value: i16) -> silk/option.Option ``` Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` @@ -206,7 +206,7 @@ this function when an out-of-range value is a program error. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: i16) -> Option +pub fn checkedToUsize(value: i16) -> silk/option.Option ``` Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` @@ -228,7 +228,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: i16) -> Option +pub fn checkedToI8(value: i16) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -250,7 +250,7 @@ can select `i16` as both source and destination. ## `checkedToI16` ```silk -pub fn checkedToI16(value: i16) -> Option +pub fn checkedToI16(value: i16) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `i16`. Use this function when generic @@ -271,7 +271,7 @@ Converts `value` exactly to `i32`. Every `i16` value is representable. ## `checkedToI32` ```silk -pub fn checkedToI32(value: i16) -> Option +pub fn checkedToI32(value: i16) -> silk/option.Option ``` Converts `value` exactly to `i32` and returns `Some`. Every `i16` value is @@ -292,7 +292,7 @@ Converts `value` exactly to `i64`. Every `i16` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: i16) -> Option +pub fn checkedToI64(value: i16) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `i16` value is @@ -313,7 +313,7 @@ Converts `value` exactly to `isize`. Every `i16` value is representable. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: i16) -> Option +pub fn checkedToIsize(value: i16) -> silk/option.Option ``` Converts `value` exactly to `isize` and returns `Some`. Every `i16` value is @@ -547,7 +547,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: i16, right: i16) -> Option +pub fn checkedAdd(left: i16, right: i16) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `i16` range. @@ -558,7 +558,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: i16, right: i16) -> Option +pub fn checkedSubtract(left: i16, right: i16) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `i16` range. @@ -569,7 +569,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: i16, right: i16) -> Option +pub fn checkedMultiply(left: i16, right: i16) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `i16` range. @@ -580,7 +580,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: i16, right: i16) -> Option +pub fn checkedDivide(left: i16, right: i16) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6931363a3a4d494e) is @@ -591,7 +591,7 @@ divided by `-1`. Use this function when an invalid quotient is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: i16, right: i16) -> Option +pub fn checkedRemainder(left: i16, right: i16) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6931363a3a4d494e) is diff --git a/apps/docs/content/language/stdlib/i32.md b/apps/docs/content/language/stdlib/i32.md index b36ee8e48..91ff0b692 100644 --- a/apps/docs/content/language/stdlib/i32.md +++ b/apps/docs/content/language/stdlib/i32.md @@ -130,7 +130,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: i32) -> Option +pub fn checkedToU8(value: i32) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -152,7 +152,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: i32) -> Option +pub fn checkedToU16(value: i32) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -174,7 +174,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: i32) -> Option +pub fn checkedToU32(value: i32) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -196,7 +196,7 @@ this function when an out-of-range value is a program error. ## `checkedToU64` ```silk -pub fn checkedToU64(value: i32) -> Option +pub fn checkedToU64(value: i32) -> silk/option.Option ``` Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` @@ -218,7 +218,7 @@ this function when an out-of-range value is a program error. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: i32) -> Option +pub fn checkedToUsize(value: i32) -> silk/option.Option ``` Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` @@ -240,7 +240,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: i32) -> Option +pub fn checkedToI8(value: i32) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -262,7 +262,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: i32) -> Option +pub fn checkedToI16(value: i32) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -284,7 +284,7 @@ can select `i32` as both source and destination. ## `checkedToI32` ```silk -pub fn checkedToI32(value: i32) -> Option +pub fn checkedToI32(value: i32) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `i32`. Use this function when generic @@ -305,7 +305,7 @@ Converts `value` exactly to `i64`. Every `i32` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: i32) -> Option +pub fn checkedToI64(value: i32) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `i32` value is @@ -326,7 +326,7 @@ Converts `value` exactly to `isize`. Every `i32` value is representable. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: i32) -> Option +pub fn checkedToIsize(value: i32) -> silk/option.Option ``` Converts `value` exactly to `isize` and returns `Some`. Every `i32` value is @@ -560,7 +560,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: i32, right: i32) -> Option +pub fn checkedAdd(left: i32, right: i32) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `i32` range. @@ -571,7 +571,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: i32, right: i32) -> Option +pub fn checkedSubtract(left: i32, right: i32) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `i32` range. @@ -582,7 +582,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: i32, right: i32) -> Option +pub fn checkedMultiply(left: i32, right: i32) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `i32` range. @@ -593,7 +593,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: i32, right: i32) -> Option +pub fn checkedDivide(left: i32, right: i32) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6933323a3a4d494e) is @@ -604,7 +604,7 @@ divided by `-1`. Use this function when an invalid quotient is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: i32, right: i32) -> Option +pub fn checkedRemainder(left: i32, right: i32) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6933323a3a4d494e) is diff --git a/apps/docs/content/language/stdlib/i64.md b/apps/docs/content/language/stdlib/i64.md index 5b58f11f3..e48281a33 100644 --- a/apps/docs/content/language/stdlib/i64.md +++ b/apps/docs/content/language/stdlib/i64.md @@ -125,7 +125,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: i64) -> Option +pub fn checkedToU8(value: i64) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -147,7 +147,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: i64) -> Option +pub fn checkedToU16(value: i64) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -169,7 +169,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: i64) -> Option +pub fn checkedToU32(value: i64) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -191,7 +191,7 @@ this function when an out-of-range value is a program error. ## `checkedToU64` ```silk -pub fn checkedToU64(value: i64) -> Option +pub fn checkedToU64(value: i64) -> silk/option.Option ``` Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` @@ -213,7 +213,7 @@ this function when an out-of-range value is a program error. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: i64) -> Option +pub fn checkedToUsize(value: i64) -> silk/option.Option ``` Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` @@ -235,7 +235,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: i64) -> Option +pub fn checkedToI8(value: i64) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -257,7 +257,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: i64) -> Option +pub fn checkedToI16(value: i64) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -279,7 +279,7 @@ this function when an out-of-range value is a program error. ## `checkedToI32` ```silk -pub fn checkedToI32(value: i64) -> Option +pub fn checkedToI32(value: i64) -> silk/option.Option ``` Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` @@ -301,7 +301,7 @@ can select `i64` as both source and destination. ## `checkedToI64` ```silk -pub fn checkedToI64(value: i64) -> Option +pub fn checkedToI64(value: i64) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `i64`. Use this function when generic @@ -323,7 +323,7 @@ this function when an out-of-range value is a program error. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: i64) -> Option +pub fn checkedToIsize(value: i64) -> silk/option.Option ``` Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` @@ -557,7 +557,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: i64, right: i64) -> Option +pub fn checkedAdd(left: i64, right: i64) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `i64` range. @@ -568,7 +568,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: i64, right: i64) -> Option +pub fn checkedSubtract(left: i64, right: i64) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `i64` range. @@ -579,7 +579,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: i64, right: i64) -> Option +pub fn checkedMultiply(left: i64, right: i64) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `i64` range. @@ -590,7 +590,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: i64, right: i64) -> Option +pub fn checkedDivide(left: i64, right: i64) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6936343a3a4d494e) is @@ -601,7 +601,7 @@ divided by `-1`. Use this function when an invalid quotient is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: i64, right: i64) -> Option +pub fn checkedRemainder(left: i64, right: i64) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6936343a3a4d494e) is diff --git a/apps/docs/content/language/stdlib/i8.md b/apps/docs/content/language/stdlib/i8.md index a28db96ee..df753e381 100644 --- a/apps/docs/content/language/stdlib/i8.md +++ b/apps/docs/content/language/stdlib/i8.md @@ -123,7 +123,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: i8) -> Option +pub fn checkedToU8(value: i8) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -145,7 +145,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: i8) -> Option +pub fn checkedToU16(value: i8) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -167,7 +167,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: i8) -> Option +pub fn checkedToU32(value: i8) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -189,7 +189,7 @@ this function when an out-of-range value is a program error. ## `checkedToU64` ```silk -pub fn checkedToU64(value: i8) -> Option +pub fn checkedToU64(value: i8) -> silk/option.Option ``` Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` @@ -211,7 +211,7 @@ this function when an out-of-range value is a program error. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: i8) -> Option +pub fn checkedToUsize(value: i8) -> silk/option.Option ``` Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` @@ -233,7 +233,7 @@ can select `i8` as both source and destination. ## `checkedToI8` ```silk -pub fn checkedToI8(value: i8) -> Option +pub fn checkedToI8(value: i8) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `i8`. Use this function when generic @@ -254,7 +254,7 @@ Converts `value` exactly to `i16`. Every `i8` value is representable. ## `checkedToI16` ```silk -pub fn checkedToI16(value: i8) -> Option +pub fn checkedToI16(value: i8) -> silk/option.Option ``` Converts `value` exactly to `i16` and returns `Some`. Every `i8` value is @@ -275,7 +275,7 @@ Converts `value` exactly to `i32`. Every `i8` value is representable. ## `checkedToI32` ```silk -pub fn checkedToI32(value: i8) -> Option +pub fn checkedToI32(value: i8) -> silk/option.Option ``` Converts `value` exactly to `i32` and returns `Some`. Every `i8` value is @@ -296,7 +296,7 @@ Converts `value` exactly to `i64`. Every `i8` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: i8) -> Option +pub fn checkedToI64(value: i8) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `i8` value is @@ -317,7 +317,7 @@ Converts `value` exactly to `isize`. Every `i8` value is representable. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: i8) -> Option +pub fn checkedToIsize(value: i8) -> silk/option.Option ``` Converts `value` exactly to `isize` and returns `Some`. Every `i8` value is @@ -551,7 +551,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: i8, right: i8) -> Option +pub fn checkedAdd(left: i8, right: i8) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `i8` range. @@ -562,7 +562,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: i8, right: i8) -> Option +pub fn checkedSubtract(left: i8, right: i8) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `i8` range. @@ -573,7 +573,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: i8, right: i8) -> Option +pub fn checkedMultiply(left: i8, right: i8) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `i8` range. @@ -584,7 +584,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: i8, right: i8) -> Option +pub fn checkedDivide(left: i8, right: i8) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f69383a3a4d494e) is @@ -595,7 +595,7 @@ divided by `-1`. Use this function when an invalid quotient is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: i8, right: i8) -> Option +pub fn checkedRemainder(left: i8, right: i8) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f69383a3a4d494e) is diff --git a/apps/docs/content/language/stdlib/index.md b/apps/docs/content/language/stdlib/index.md index 5fa021992..a07c1e37b 100644 --- a/apps/docs/content/language/stdlib/index.md +++ b/apps/docs/content/language/stdlib/index.md @@ -36,7 +36,7 @@ The compiler-shipped modules and their complete public documentation. | [`silk/metrics`](./metrics.md) | `AllocationMetrics` | 6 | Provider-owned allocation counters that can be published as ordinary copyable data. | | [`silk/monotonic_clock`](./monotonic-clock.md) | `MonotonicClock` | 5 | Provider-replaceable monotonic marks, resolution, and deadline waits. | | [`silk/numeric`](./numeric.md) | `Integer` | 2 | Shared compile-time addition witness for generic algorithms over primitive integers. | -| [`silk/option`](./option.md) | `Option` | 8 | Optional owned values that distinguish presence from absence without a failure channel. | +| [`silk/option`](./option.md) | `Option` | 6 | Optional owned values that distinguish presence from absence without a failure channel. | | [`silk/order`](./order.md) | `Order` | 11 | Compile-time ordering witnesses and a three-way result derived from strict comparison. | | [`silk/os_child_process`](./os-child-process.md) | `OsChildProcess` | 2 | Native ChildProcess provider that executes directly through the platform process boundary. | | [`silk/os_filesystem`](./os-filesystem.md) | `OsFileSystem` | 2 | Native FileSystem provider confined beneath one explicitly owned platform root. | @@ -47,7 +47,7 @@ The compiler-shipped modules and their complete public documentation. | [`silk/os_system_clock`](./os-system-clock.md) | `OsSystemClock` | 2 | Native Unix-epoch clock provider for SystemClock. | | [`silk/random`](./random.md) | `Random` | 5 | Provider-replaceable cryptographically secure random bytes and derived values. | | [`silk/raw_buffer`](./raw-buffer.md) | `RawBuffer` | 8 | Low-level typed views over owned allocations for implementing collections and storage actors. | -| [`silk/result`](./result.md) | `Result` | 11 | Completed success-or-failure values that can be inspected and transformed as ordinary data. | +| [`silk/result`](./result.md) | `Result` | 7 | Completed success-or-failure values that can be inspected and transformed as ordinary data. | | [`silk/scheduler`](./scheduler.md) | `Scheduler` | 17 | Provider protocol for preparing and atomically publishing child Fibers. | | [`silk/shared`](./shared.md) | `Shared` | 5 | Explicitly allocated, single-threaded shared ownership with callback-scoped access. | | [`silk/slot`](./slot.md) | `Slot` | 4 | Explicit initialization-state transitions for one slot selected from raw storage. | diff --git a/apps/docs/content/language/stdlib/insecure-random.md b/apps/docs/content/language/stdlib/insecure-random.md index 5575399d6..f339a668d 100644 --- a/apps/docs/content/language/stdlib/insecure-random.md +++ b/apps/docs/content/language/stdlib/insecure-random.md @@ -137,7 +137,7 @@ Returns whether bit 63 of the next deterministic provider word is set. ## `below` ```silk -pub effect fn below(upperExclusive: u64) -> Option ? &mut InsecureRandom +pub effect fn below(upperExclusive: u64) -> silk/option.Option ? &mut InsecureRandom ``` Returns a deterministic value below `upperExclusive`, or `None` when the bound is zero. diff --git a/apps/docs/content/language/stdlib/isize.md b/apps/docs/content/language/stdlib/isize.md index fa6ceb1dd..1f8ea91e2 100644 --- a/apps/docs/content/language/stdlib/isize.md +++ b/apps/docs/content/language/stdlib/isize.md @@ -131,7 +131,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: isize) -> Option +pub fn checkedToU8(value: isize) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -153,7 +153,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: isize) -> Option +pub fn checkedToU16(value: isize) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -175,7 +175,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: isize) -> Option +pub fn checkedToU32(value: isize) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -197,7 +197,7 @@ this function when an out-of-range value is a program error. ## `checkedToU64` ```silk -pub fn checkedToU64(value: isize) -> Option +pub fn checkedToU64(value: isize) -> silk/option.Option ``` Converts `value` to `u64`, or returns `None` if `value` is outside the `u64` @@ -219,7 +219,7 @@ this function when an out-of-range value is a program error. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: isize) -> Option +pub fn checkedToUsize(value: isize) -> silk/option.Option ``` Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` @@ -241,7 +241,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: isize) -> Option +pub fn checkedToI8(value: isize) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -263,7 +263,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: isize) -> Option +pub fn checkedToI16(value: isize) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -285,7 +285,7 @@ this function when an out-of-range value is a program error. ## `checkedToI32` ```silk -pub fn checkedToI32(value: isize) -> Option +pub fn checkedToI32(value: isize) -> silk/option.Option ``` Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` @@ -306,7 +306,7 @@ Converts `value` exactly to `i64`. Every `isize` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: isize) -> Option +pub fn checkedToI64(value: isize) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `isize` value is @@ -328,7 +328,7 @@ can select `isize` as both source and destination. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: isize) -> Option +pub fn checkedToIsize(value: isize) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `isize`. Use this function when generic @@ -562,7 +562,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: isize, right: isize) -> Option +pub fn checkedAdd(left: isize, right: isize) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `isize` range. @@ -573,7 +573,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: isize, right: isize) -> Option +pub fn checkedSubtract(left: isize, right: isize) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `isize` range. @@ -584,7 +584,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: isize, right: isize) -> Option +pub fn checkedMultiply(left: isize, right: isize) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `isize` range. @@ -595,7 +595,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: isize, right: isize) -> Option +pub fn checkedDivide(left: isize, right: isize) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6973697a653a3a4d494e) is @@ -606,7 +606,7 @@ divided by `-1`. Use this function when an invalid quotient is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: isize, right: isize) -> Option +pub fn checkedRemainder(left: isize, right: isize) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero or [`MIN`](#declaration-73696c6b2f6973697a653a3a4d494e) is diff --git a/apps/docs/content/language/stdlib/option.md b/apps/docs/content/language/stdlib/option.md index c66fda39d..c0b9427aa 100644 --- a/apps/docs/content/language/stdlib/option.md +++ b/apps/docs/content/language/stdlib/option.md @@ -12,7 +12,7 @@ when the caller is ready to consume the option. ## Details -`Option` is the structural union of [`Some`](#declaration-73696c6b2f6f7074696f6e3a3a536f6d65) and [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65). Its combinators preserve affine +`Option` is a nominal union with `Some` and `None` variants. Its combinators preserve affine ownership: a present value moves forward, while an unused fallback or abandoned branch drops. ## Examples @@ -47,49 +47,49 @@ pub fn main() -> i32 { Import as `Option` with `import silk.option`. -Public declarations: 8. +Public declarations: 6. - + -## `Some` +## `Option` ```silk -pub struct Some +pub union Option ``` -The present member of [`Option`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e), carrying the available owned value. +An owned value that is either `Some` or `None`. + +### Details + +Match on an `Option` when both arms need custom behavior. Prefer [`map`](#declaration-73696c6b2f6f7074696f6e3a3a6d6170), [`flatMap`](#declaration-73696c6b2f6f7074696f6e3a3a666c61744d6170), or +[`unwrapOr`](#declaration-73696c6b2f6f7074696f6e3a3a756e777261704f72) for the common transform, continue, and default cases. - + -## `None` +### `None` ```silk -pub struct None +Option.None: Option ``` -The absent member of [`Option`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e); it carries no explanation for the absence. +The absent variant; it carries no explanation for the absence. - + -## `Option` +### `Some` ```silk -pub struct Option +Option.Some { value: T }: Option ``` -An owned value that is either [`Some`](#declaration-73696c6b2f6f7074696f6e3a3a536f6d65) or [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65). - -### Details - -Match on an `Option` when both arms need custom behavior. Prefer [`map`](#declaration-73696c6b2f6f7074696f6e3a3a6d6170), [`flatMap`](#declaration-73696c6b2f6f7074696f6e3a3a666c61744d6170), or -[`unwrapOr`](#declaration-73696c6b2f6f7074696f6e3a3a756e777261704f72) for the common transform, continue, and default cases. +The present variant, carrying the available owned value. ## `none` ```silk -pub fn none() -> Option +pub fn none() -> silk/option.Option ``` Constructs an absent optional value of the requested element type. @@ -99,7 +99,7 @@ Constructs an absent optional value of the requested element type. ## `some` ```silk -pub fn some(value: T) -> Option +pub fn some(value: T) -> silk/option.Option ``` Constructs a present option by moving `value` into it. @@ -109,14 +109,14 @@ Constructs a present option by moving `value` into it. ## `map` ```silk -pub fn map(self: Option, transform: once fn(T) -> U) -> Option +pub fn map(self: silk/option.Option, transform: once fn(T) -> U) -> silk/option.Option ``` Applies `transform` once to a present value and keeps an absent value absent. ### Details -The callback is not called for [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65). This operation consumes `self`; use a shared borrow and +The callback is not called for `None`. This operation consumes `self`; use a shared borrow and `match` instead when the original option must remain available. @@ -124,7 +124,7 @@ The callback is not called for [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4 ## `flatMap` ```silk -pub fn flatMap(self: Option, transform: once fn(T) -> Option) -> Option +pub fn flatMap(self: silk/option.Option, transform: once fn(T) -> silk/option.Option) -> silk/option.Option ``` Continues a present value with a transform that itself answers with an Option, so the @@ -132,7 +132,7 @@ outcome stays one Option deep instead of nesting. ### Details -The callback runs once for [`Some`](#declaration-73696c6b2f6f7074696f6e3a3a536f6d65) and not at all for [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65). Use this when the next step may +The callback runs once for `Some` and not at all for `None`. Use this when the next step may reject the value without needing to explain why; use a `Result` when rejection needs an error. @@ -140,7 +140,7 @@ reject the value without needing to explain why; use a `Result` when rejection n ## `unwrapOr` ```silk -pub fn unwrapOr(self: Option, fallback: T) -> T +pub fn unwrapOr(self: silk/option.Option, fallback: T) -> T ``` Returns the present value, or the fallback value when the option is absent. diff --git a/apps/docs/content/language/stdlib/os-host-input.md b/apps/docs/content/language/stdlib/os-host-input.md index f321d544b..2a8f5196e 100644 --- a/apps/docs/content/language/stdlib/os-host-input.md +++ b/apps/docs/content/language/stdlib/os-host-input.md @@ -14,7 +14,7 @@ the program under test. The provider owns no persistent state. Each successful lookup copies the host value into independent [`Bytes`](./bytes.md#declaration-73696c6b2f62797465733a3a4279746573), beginning with a bounded buffer and retrying once at the exact size the -boundary reports. An absent argument or variable remains [`None`](./option.md#declaration-73696c6b2f6f7074696f6e3a3a4e6f6e65); an unavailable working +boundary reports. An absent argument or variable remains `None`; an unavailable working directory and contradictory host lengths become [`HostInputError`](./host-input.md#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e7075744572726f72). Constructing the provider reads no host state. Portable code performs lookups after the diff --git a/apps/docs/content/language/stdlib/random.md b/apps/docs/content/language/stdlib/random.md index b39b369e0..abe33b608 100644 --- a/apps/docs/content/language/stdlib/random.md +++ b/apps/docs/content/language/stdlib/random.md @@ -102,7 +102,7 @@ Returns whether bit 63 of one secure provider word is set. ## `below` ```silk -pub effect fn below(upperExclusive: u64) -> Option ? &mut Random +pub effect fn below(upperExclusive: u64) -> silk/option.Option ? &mut Random ``` Returns an unbiased secure value below `upperExclusive`, or `None` for zero. diff --git a/apps/docs/content/language/stdlib/result.md b/apps/docs/content/language/stdlib/result.md index 5ef052e77..81a6ecc2c 100644 --- a/apps/docs/content/language/stdlib/result.md +++ b/apps/docs/content/language/stdlib/result.md @@ -12,7 +12,7 @@ success continuation that already returns a result. ## Details -`Result` owns either [`Success`](#declaration-73696c6b2f726573756c743a3a53756363657373) or [`Failure`](#declaration-73696c6b2f726573756c743a3a4661696c757265). Its combinators move the selected payload +`Result` owns either `Success` or `Failure`. Its combinators move the selected payload forward and preserve the other arm without inventing a runtime failure-row descriptor. Unlike an `Effect`, a `Result` is already completed ordinary data: it does not run, @@ -41,55 +41,51 @@ pub fn main() -> i32 { let initial = Result.succeed(80) let halved = Result.flatMap(move initial, half) let answer = Result.map(move halved, addTwo) - let failed = Result.failResult(7) - if Result.isFailure(&failed) {} else { - return 0 - } return Result.unwrapOr(move answer, 0) } ``` Import as `Result` with `import silk.result`. -Public declarations: 11. +Public declarations: 7. - + -## `Success` +## `Result` ```silk -pub struct Success +pub union Result ``` -The successful member of a completed [`Result`](#declaration-73696c6b2f726573756c743a3a526573756c74). +One completed outcome: either a success carrying `A` or a failure carrying `F`. + +### Details + +`Result` is the reified form of an Effect that has already run. Reifying an Effect turns its +failure row into ordinary value data, which is what lets the failure combinators in +`silk.effect` be written as ordinary Silk source instead of compiler built-ins. +A `Result` is consumed when matched or passed to a transforming combinator. Use a borrowed +match when the payload must remain available. - + -## `Failure` +### `Success` ```silk -pub struct Failure +Result.Success { value: A }: Result ``` -The failed member of a completed [`Result`](#declaration-73696c6b2f726573756c743a3a526573756c74). +A completed success. - + -## `Result` +### `Failure` ```silk -pub struct Result +Result.Failure { error: F }: Result ``` -One completed outcome: either a success carrying `A` or a failure carrying `F`. - -### Details - -`Result` is the reified form of an Effect that has already run. Reifying an Effect turns its -failure row into ordinary value data, which is what lets the failure combinators in -`silk.effect` be written as ordinary Silk source instead of compiler built-ins. -A `Result` is consumed when matched or passed to a transforming combinator; borrow it for -[`isSuccess`](#declaration-73696c6b2f726573756c743a3a697353756363657373) and [`isFailure`](#declaration-73696c6b2f726573756c743a3a69734661696c757265) when the payload must remain available. +A completed failure. @@ -123,7 +119,7 @@ Applies `transform` once to a success value and carries a failure through unchan ### Details -The callback is never called for [`Failure`](#declaration-73696c6b2f726573756c743a3a4661696c757265). This consumes the result and may change only its +The callback is never called for `Failure`. This consumes the result and may change only its success type; use [`mapError`](#declaration-73696c6b2f726573756c743a3a6d61704572726f72) to change the failure type instead. @@ -138,7 +134,7 @@ Applies `transform` once to a failure value and carries a success through unchan ### Details -The callback is never called for [`Success`](#declaration-73696c6b2f726573756c743a3a53756363657373). This consumes the result and may change only its +The callback is never called for `Success`. This consumes the result and may change only its failure type. @@ -182,23 +178,3 @@ fallback: A ``` The owned alternative consumed only when `self` is a failure. - - - -## `isSuccess` - -```silk -pub fn isSuccess(self: &silk/result.Result) -> bool -``` - -Returns `true` when the borrowed outcome is [`Success`](#declaration-73696c6b2f726573756c743a3a53756363657373), without consuming either payload. - - - -## `isFailure` - -```silk -pub fn isFailure(self: &silk/result.Result) -> bool -``` - -Returns `true` when the borrowed outcome is [`Failure`](#declaration-73696c6b2f726573756c743a3a4661696c757265), without consuming either payload. diff --git a/apps/docs/content/language/stdlib/string.md b/apps/docs/content/language/stdlib/string.md index 2fb8d1cd5..35b8746d1 100644 --- a/apps/docs/content/language/stdlib/string.md +++ b/apps/docs/content/language/stdlib/string.md @@ -35,8 +35,9 @@ pub fn main() -> i32 { let valid = String.fromUtf8(b"Silk") |> Result.unwrapOr("") let invalid = String.fromUtf8(b"a\x80") - if !Result.isFailure(&invalid) { - return 0 + match move invalid { + Result.Result.Success { .. } => return 0 + Result.Result.Failure { .. } => () } let length = String.byteLength(valid) |> usize.toI32 @@ -359,7 +360,7 @@ Consumes one scalar step and returns the cursor immediately after that scalar. ## `nextScalar` ```silk -pub fn nextScalar(value: string, cursor: ScalarCursor) -> Option +pub fn nextScalar(value: string, cursor: ScalarCursor) -> silk/option.Option ``` Decodes the scalar at a cursor, or returns `None` at the end of the string. diff --git a/apps/docs/content/language/stdlib/u16.md b/apps/docs/content/language/stdlib/u16.md index 30bc6c580..78b44a207 100644 --- a/apps/docs/content/language/stdlib/u16.md +++ b/apps/docs/content/language/stdlib/u16.md @@ -87,7 +87,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: u16) -> Option +pub fn checkedToU8(value: u16) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -109,7 +109,7 @@ can select `u16` as both source and destination. ## `checkedToU16` ```silk -pub fn checkedToU16(value: u16) -> Option +pub fn checkedToU16(value: u16) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `u16`. Use this function when generic @@ -130,7 +130,7 @@ Converts `value` exactly to `u32`. Every `u16` value is representable. ## `checkedToU32` ```silk -pub fn checkedToU32(value: u16) -> Option +pub fn checkedToU32(value: u16) -> silk/option.Option ``` Converts `value` exactly to `u32` and returns `Some`. Every `u16` value is @@ -151,7 +151,7 @@ Converts `value` exactly to `u64`. Every `u16` value is representable. ## `checkedToU64` ```silk -pub fn checkedToU64(value: u16) -> Option +pub fn checkedToU64(value: u16) -> silk/option.Option ``` Converts `value` exactly to `u64` and returns `Some`. Every `u16` value is @@ -172,7 +172,7 @@ Converts `value` exactly to `usize`. Every `u16` value is representable. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: u16) -> Option +pub fn checkedToUsize(value: u16) -> silk/option.Option ``` Converts `value` exactly to `usize` and returns `Some`. Every `u16` value is @@ -194,7 +194,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: u16) -> Option +pub fn checkedToI8(value: u16) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -216,7 +216,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: u16) -> Option +pub fn checkedToI16(value: u16) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -237,7 +237,7 @@ Converts `value` exactly to `i32`. Every `u16` value is representable. ## `checkedToI32` ```silk -pub fn checkedToI32(value: u16) -> Option +pub fn checkedToI32(value: u16) -> silk/option.Option ``` Converts `value` exactly to `i32` and returns `Some`. Every `u16` value is @@ -258,7 +258,7 @@ Converts `value` exactly to `i64`. Every `u16` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: u16) -> Option +pub fn checkedToI64(value: u16) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `u16` value is @@ -279,7 +279,7 @@ Converts `value` exactly to `isize`. Every `u16` value is representable. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: u16) -> Option +pub fn checkedToIsize(value: u16) -> silk/option.Option ``` Converts `value` exactly to `isize` and returns `Some`. Every `u16` value is @@ -512,7 +512,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: u16, right: u16) -> Option +pub fn checkedAdd(left: u16, right: u16) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `u16` range. @@ -523,7 +523,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: u16, right: u16) -> Option +pub fn checkedSubtract(left: u16, right: u16) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `u16` range. @@ -534,7 +534,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: u16, right: u16) -> Option +pub fn checkedMultiply(left: u16, right: u16) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `u16` range. @@ -545,7 +545,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: u16, right: u16) -> Option +pub fn checkedDivide(left: u16, right: u16) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero. Use this @@ -556,7 +556,7 @@ function when a zero divisor is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: u16, right: u16) -> Option +pub fn checkedRemainder(left: u16, right: u16) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero. Use this function diff --git a/apps/docs/content/language/stdlib/u32.md b/apps/docs/content/language/stdlib/u32.md index 39254bf62..a157ed1e2 100644 --- a/apps/docs/content/language/stdlib/u32.md +++ b/apps/docs/content/language/stdlib/u32.md @@ -82,7 +82,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: u32) -> Option +pub fn checkedToU8(value: u32) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -104,7 +104,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: u32) -> Option +pub fn checkedToU16(value: u32) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -126,7 +126,7 @@ can select `u32` as both source and destination. ## `checkedToU32` ```silk -pub fn checkedToU32(value: u32) -> Option +pub fn checkedToU32(value: u32) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `u32`. Use this function when generic @@ -147,7 +147,7 @@ Converts `value` exactly to `u64`. Every `u32` value is representable. ## `checkedToU64` ```silk -pub fn checkedToU64(value: u32) -> Option +pub fn checkedToU64(value: u32) -> silk/option.Option ``` Converts `value` exactly to `u64` and returns `Some`. Every `u32` value is @@ -168,7 +168,7 @@ Converts `value` exactly to `usize`. Every `u32` value is representable. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: u32) -> Option +pub fn checkedToUsize(value: u32) -> silk/option.Option ``` Converts `value` exactly to `usize` and returns `Some`. Every `u32` value is @@ -190,7 +190,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: u32) -> Option +pub fn checkedToI8(value: u32) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -212,7 +212,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: u32) -> Option +pub fn checkedToI16(value: u32) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -234,7 +234,7 @@ this function when an out-of-range value is a program error. ## `checkedToI32` ```silk -pub fn checkedToI32(value: u32) -> Option +pub fn checkedToI32(value: u32) -> silk/option.Option ``` Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` @@ -255,7 +255,7 @@ Converts `value` exactly to `i64`. Every `u32` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: u32) -> Option +pub fn checkedToI64(value: u32) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `u32` value is @@ -277,7 +277,7 @@ this function when an out-of-range value is a program error. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: u32) -> Option +pub fn checkedToIsize(value: u32) -> silk/option.Option ``` Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` @@ -510,7 +510,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: u32, right: u32) -> Option +pub fn checkedAdd(left: u32, right: u32) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `u32` range. @@ -521,7 +521,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: u32, right: u32) -> Option +pub fn checkedSubtract(left: u32, right: u32) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `u32` range. @@ -532,7 +532,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: u32, right: u32) -> Option +pub fn checkedMultiply(left: u32, right: u32) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `u32` range. @@ -543,7 +543,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: u32, right: u32) -> Option +pub fn checkedDivide(left: u32, right: u32) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero. Use this @@ -554,7 +554,7 @@ function when a zero divisor is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: u32, right: u32) -> Option +pub fn checkedRemainder(left: u32, right: u32) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero. Use this function diff --git a/apps/docs/content/language/stdlib/u64.md b/apps/docs/content/language/stdlib/u64.md index b262ebc61..664e42fb2 100644 --- a/apps/docs/content/language/stdlib/u64.md +++ b/apps/docs/content/language/stdlib/u64.md @@ -86,7 +86,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: u64) -> Option +pub fn checkedToU8(value: u64) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -108,7 +108,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: u64) -> Option +pub fn checkedToU16(value: u64) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -130,7 +130,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: u64) -> Option +pub fn checkedToU32(value: u64) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -152,7 +152,7 @@ can select `u64` as both source and destination. ## `checkedToU64` ```silk -pub fn checkedToU64(value: u64) -> Option +pub fn checkedToU64(value: u64) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `u64`. Use this function when generic @@ -174,7 +174,7 @@ this function when an out-of-range value is a program error. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: u64) -> Option +pub fn checkedToUsize(value: u64) -> silk/option.Option ``` Converts `value` to `usize`, or returns `None` if `value` is outside the `usize` @@ -196,7 +196,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: u64) -> Option +pub fn checkedToI8(value: u64) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -218,7 +218,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: u64) -> Option +pub fn checkedToI16(value: u64) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -240,7 +240,7 @@ this function when an out-of-range value is a program error. ## `checkedToI32` ```silk -pub fn checkedToI32(value: u64) -> Option +pub fn checkedToI32(value: u64) -> silk/option.Option ``` Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` @@ -262,7 +262,7 @@ this function when an out-of-range value is a program error. ## `checkedToI64` ```silk -pub fn checkedToI64(value: u64) -> Option +pub fn checkedToI64(value: u64) -> silk/option.Option ``` Converts `value` to `i64`, or returns `None` if `value` is outside the `i64` @@ -284,7 +284,7 @@ this function when an out-of-range value is a program error. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: u64) -> Option +pub fn checkedToIsize(value: u64) -> silk/option.Option ``` Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` @@ -517,7 +517,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: u64, right: u64) -> Option +pub fn checkedAdd(left: u64, right: u64) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `u64` range. @@ -528,7 +528,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: u64, right: u64) -> Option +pub fn checkedSubtract(left: u64, right: u64) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `u64` range. @@ -539,7 +539,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: u64, right: u64) -> Option +pub fn checkedMultiply(left: u64, right: u64) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `u64` range. @@ -550,7 +550,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: u64, right: u64) -> Option +pub fn checkedDivide(left: u64, right: u64) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero. Use this @@ -561,7 +561,7 @@ function when a zero divisor is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: u64, right: u64) -> Option +pub fn checkedRemainder(left: u64, right: u64) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero. Use this function diff --git a/apps/docs/content/language/stdlib/u8.md b/apps/docs/content/language/stdlib/u8.md index c9b4e3087..d72bef23f 100644 --- a/apps/docs/content/language/stdlib/u8.md +++ b/apps/docs/content/language/stdlib/u8.md @@ -98,7 +98,7 @@ can select `u8` as both source and destination. ## `checkedToU8` ```silk -pub fn checkedToU8(value: u8) -> Option +pub fn checkedToU8(value: u8) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `u8`. Use this function when generic @@ -119,7 +119,7 @@ Converts `value` exactly to `u16`. Every `u8` value is representable. ## `checkedToU16` ```silk -pub fn checkedToU16(value: u8) -> Option +pub fn checkedToU16(value: u8) -> silk/option.Option ``` Converts `value` exactly to `u16` and returns `Some`. Every `u8` value is @@ -140,7 +140,7 @@ Converts `value` exactly to `u32`. Every `u8` value is representable. ## `checkedToU32` ```silk -pub fn checkedToU32(value: u8) -> Option +pub fn checkedToU32(value: u8) -> silk/option.Option ``` Converts `value` exactly to `u32` and returns `Some`. Every `u8` value is @@ -161,7 +161,7 @@ Converts `value` exactly to `u64`. Every `u8` value is representable. ## `checkedToU64` ```silk -pub fn checkedToU64(value: u8) -> Option +pub fn checkedToU64(value: u8) -> silk/option.Option ``` Converts `value` exactly to `u64` and returns `Some`. Every `u8` value is @@ -182,7 +182,7 @@ Converts `value` exactly to `usize`. Every `u8` value is representable. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: u8) -> Option +pub fn checkedToUsize(value: u8) -> silk/option.Option ``` Converts `value` exactly to `usize` and returns `Some`. Every `u8` value is @@ -204,7 +204,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: u8) -> Option +pub fn checkedToI8(value: u8) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -225,7 +225,7 @@ Converts `value` exactly to `i16`. Every `u8` value is representable. ## `checkedToI16` ```silk -pub fn checkedToI16(value: u8) -> Option +pub fn checkedToI16(value: u8) -> silk/option.Option ``` Converts `value` exactly to `i16` and returns `Some`. Every `u8` value is @@ -246,7 +246,7 @@ Converts `value` exactly to `i32`. Every `u8` value is representable. ## `checkedToI32` ```silk -pub fn checkedToI32(value: u8) -> Option +pub fn checkedToI32(value: u8) -> silk/option.Option ``` Converts `value` exactly to `i32` and returns `Some`. Every `u8` value is @@ -267,7 +267,7 @@ Converts `value` exactly to `i64`. Every `u8` value is representable. ## `checkedToI64` ```silk -pub fn checkedToI64(value: u8) -> Option +pub fn checkedToI64(value: u8) -> silk/option.Option ``` Converts `value` exactly to `i64` and returns `Some`. Every `u8` value is @@ -288,7 +288,7 @@ Converts `value` exactly to `isize`. Every `u8` value is representable. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: u8) -> Option +pub fn checkedToIsize(value: u8) -> silk/option.Option ``` Converts `value` exactly to `isize` and returns `Some`. Every `u8` value is @@ -521,7 +521,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: u8, right: u8) -> Option +pub fn checkedAdd(left: u8, right: u8) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `u8` range. @@ -532,7 +532,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: u8, right: u8) -> Option +pub fn checkedSubtract(left: u8, right: u8) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `u8` range. @@ -543,7 +543,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: u8, right: u8) -> Option +pub fn checkedMultiply(left: u8, right: u8) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `u8` range. @@ -554,7 +554,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: u8, right: u8) -> Option +pub fn checkedDivide(left: u8, right: u8) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero. Use this @@ -565,7 +565,7 @@ function when a zero divisor is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: u8, right: u8) -> Option +pub fn checkedRemainder(left: u8, right: u8) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero. Use this function diff --git a/apps/docs/content/language/stdlib/usize.md b/apps/docs/content/language/stdlib/usize.md index 3aeba3267..dfd20c398 100644 --- a/apps/docs/content/language/stdlib/usize.md +++ b/apps/docs/content/language/stdlib/usize.md @@ -115,7 +115,7 @@ this function when an out-of-range value is a program error. ## `checkedToU8` ```silk -pub fn checkedToU8(value: usize) -> Option +pub fn checkedToU8(value: usize) -> silk/option.Option ``` Converts `value` to `u8`, or returns `None` if `value` is outside the `u8` @@ -137,7 +137,7 @@ this function when an out-of-range value is a program error. ## `checkedToU16` ```silk -pub fn checkedToU16(value: usize) -> Option +pub fn checkedToU16(value: usize) -> silk/option.Option ``` Converts `value` to `u16`, or returns `None` if `value` is outside the `u16` @@ -159,7 +159,7 @@ this function when an out-of-range value is a program error. ## `checkedToU32` ```silk -pub fn checkedToU32(value: usize) -> Option +pub fn checkedToU32(value: usize) -> silk/option.Option ``` Converts `value` to `u32`, or returns `None` if `value` is outside the `u32` @@ -180,7 +180,7 @@ Converts `value` exactly to `u64`. Every `usize` value is representable. ## `checkedToU64` ```silk -pub fn checkedToU64(value: usize) -> Option +pub fn checkedToU64(value: usize) -> silk/option.Option ``` Converts `value` exactly to `u64` and returns `Some`. Every `usize` value is @@ -202,7 +202,7 @@ can select `usize` as both source and destination. ## `checkedToUsize` ```silk -pub fn checkedToUsize(value: usize) -> Option +pub fn checkedToUsize(value: usize) -> silk/option.Option ``` Returns `Some` with `value` unchanged as `usize`. Use this function when generic @@ -224,7 +224,7 @@ this function when an out-of-range value is a program error. ## `checkedToI8` ```silk -pub fn checkedToI8(value: usize) -> Option +pub fn checkedToI8(value: usize) -> silk/option.Option ``` Converts `value` to `i8`, or returns `None` if `value` is outside the `i8` @@ -246,7 +246,7 @@ this function when an out-of-range value is a program error. ## `checkedToI16` ```silk -pub fn checkedToI16(value: usize) -> Option +pub fn checkedToI16(value: usize) -> silk/option.Option ``` Converts `value` to `i16`, or returns `None` if `value` is outside the `i16` @@ -268,7 +268,7 @@ this function when an out-of-range value is a program error. ## `checkedToI32` ```silk -pub fn checkedToI32(value: usize) -> Option +pub fn checkedToI32(value: usize) -> silk/option.Option ``` Converts `value` to `i32`, or returns `None` if `value` is outside the `i32` @@ -290,7 +290,7 @@ this function when an out-of-range value is a program error. ## `checkedToI64` ```silk -pub fn checkedToI64(value: usize) -> Option +pub fn checkedToI64(value: usize) -> silk/option.Option ``` Converts `value` to `i64`, or returns `None` if `value` is outside the `i64` @@ -312,7 +312,7 @@ this function when an out-of-range value is a program error. ## `checkedToIsize` ```silk -pub fn checkedToIsize(value: usize) -> Option +pub fn checkedToIsize(value: usize) -> silk/option.Option ``` Converts `value` to `isize`, or returns `None` if `value` is outside the `isize` @@ -545,7 +545,7 @@ boundary value is the required overflow result. ## `checkedAdd` ```silk -pub fn checkedAdd(left: usize, right: usize) -> Option +pub fn checkedAdd(left: usize, right: usize) -> silk/option.Option ``` Returns `Some` with `left + right`, or `None` if the result is outside the `usize` range. @@ -556,7 +556,7 @@ Use this function when overflow is input data. ## `checkedSubtract` ```silk -pub fn checkedSubtract(left: usize, right: usize) -> Option +pub fn checkedSubtract(left: usize, right: usize) -> silk/option.Option ``` Returns `Some` with `left - right`, or `None` if the result is outside the `usize` range. @@ -567,7 +567,7 @@ Use this function when overflow is input data. ## `checkedMultiply` ```silk -pub fn checkedMultiply(left: usize, right: usize) -> Option +pub fn checkedMultiply(left: usize, right: usize) -> silk/option.Option ``` Returns `Some` with `left * right`, or `None` if the result is outside the `usize` range. @@ -578,7 +578,7 @@ Use this function when overflow is input data. ## `checkedDivide` ```silk -pub fn checkedDivide(left: usize, right: usize) -> Option +pub fn checkedDivide(left: usize, right: usize) -> silk/option.Option ``` Returns `Some` with `left / right`, or `None` if `right` is zero. Use this @@ -589,7 +589,7 @@ function when a zero divisor is input data. ## `checkedRemainder` ```silk -pub fn checkedRemainder(left: usize, right: usize) -> Option +pub fn checkedRemainder(left: usize, right: usize) -> silk/option.Option ``` Returns `Some` with the remainder, or `None` if `right` is zero. Use this function diff --git a/apps/docs/content/language/stdlib/vector.md b/apps/docs/content/language/stdlib/vector.md index 71ffad0d3..b4e748a6f 100644 --- a/apps/docs/content/language/stdlib/vector.md +++ b/apps/docs/content/language/stdlib/vector.md @@ -285,7 +285,7 @@ Use this function for a `Copy` element. Use [`asSlice`](#declaration-73696c6b2f7 ## `pop` ```silk -pub fn pop(self: &mut silk/vector.Vector) -> Option +pub fn pop(self: &mut silk/vector.Vector) -> silk/option.Option ``` Removes the last element and returns it. Returns an absent value for an empty vector. @@ -377,7 +377,7 @@ storage. If allocation fails, the vector remains unchanged. ## `binarySearch` ```silk -pub fn binarySearch(self: &silk/vector.Vector, target: T) -> Option +pub fn binarySearch(self: &silk/vector.Vector, target: T) -> silk/option.Option ``` Returns the index of a matching element in a sorted vector, or an absent value when none matches. diff --git a/apps/docs/content/reference/effect-contracts.md b/apps/docs/content/reference/effect-contracts.md index 53fde1c1a..180afcf17 100644 --- a/apps/docs/content/reference/effect-contracts.md +++ b/apps/docs/content/reference/effect-contracts.md @@ -338,3 +338,54 @@ contract, access, ownership, lifetime, or finite-representation boundary that fa Typed-failure compatibility, requirement membership, and Effect execution access intentionally remain for their own reference areas. + +## EFF-014 — Completed outcomes are reified by ordinary composition + +**Status:** Confirmed + +`Effect.result` runs one protected `Effect` and returns +`Result` while preserving `R`. It first maps success into +`Result.Success`, then uses the general `Effect.catchAll` operation to map the complete failure +value into `Result.Failure`. + +```silk +import silk.effect as Effect +import silk.result as Result + +struct HttpError {} +struct OutOfMemoryError {} + +effect fn fetch() -> i32 ! HttpError | OutOfMemoryError { + fail HttpError {} +} + +effect fn inspect() -> Result.Result { + return run Effect.result(fetch()) +} +``` + +The outer `Result` remains one nominal union. Its `Failure` payload is the ordinary structural union +`HttpError | OutOfMemoryError`; reification does not flatten either layer. The two constructor +adapters are exact `once fn` values, so success and failure payloads may be move-only and are +transferred exactly once. Traps are not typed failures and are not converted into `Failure`. + +This behavior is not privileged. Another library can define a generic result-like nominal union and +compose the same `map` followed by `catchAll` under any legal function name. The compiler recognizes +neither `Effect.result`, `Result`, nor its variants by spelling, and exposes no completed-outcome +intrinsic. + +**Boundary:** `Effect.result` handles the complete declared failure type `E`; selective recovery +remains the role of `Effect.catch`. Reification does not execute more than one Effect layer, remove +requirements, erase ownership, expose a pending state, or catch traps and future interruption. + +**Diagnostics:** Invalid callbacks, failure carriers, ownership transfers, or requirement rows use +the ordinary callable, Effect, ownership, and row diagnostics. There is no diagnostic or semantic +branch specific to the names `Effect.result`, `Result`, `Success`, or `Failure`. + +**Current compiler:** Aligned. The standard-library implementation is ordinary Silk source over +`map` and `catchAll`; the compiler has no `Intrinsic.effectResult` operation or replacement +completed-outcome primitive. + +**Evidence:** [ordinary source implementation](../../../../packages/compiler/stdlib/silk/effect.silk), +[Result and alternate-carrier regressions](../../../../packages/compiler/test/ResultStdlib.test.ts), +[minimal intrinsic boundary](../../../../openspec/changes/add-nominal-unions/specs/bootstrap-intrinsic-boundary/spec.md). diff --git a/apps/docs/content/reference/expressions-and-operators.md b/apps/docs/content/reference/expressions-and-operators.md index 68a7f7ba9..c8450e39c 100644 --- a/apps/docs/content/reference/expressions-and-operators.md +++ b/apps/docs/content/reference/expressions-and-operators.md @@ -444,7 +444,7 @@ Effect failure channel. When overflow or invalid division is recoverable applica named `checked*` operation returning `Option`. When modular or clamped arithmetic is intended, use the corresponding named `wrapping*` or `saturating*` operation where supplied. -For every signed width, `checkedRemainder(MIN, -1)` returns `None` on every executor exactly where +For every signed width, `checkedRemainder(MIN, -1)` returns `Option.None` on every executor exactly where ordinary `%` traps. It never reaches a backend remainder instruction whose behavior differs at that boundary. @@ -1034,7 +1034,7 @@ functions use the ordinary actor-operation diagnostic. An integer actor's `toX` operation returns the destination integer type and traps when the source value is outside its range. The corresponding `checkedToX` operation returns `Option`, producing -`Some` when representable and `None` otherwise. +`Option.Some` when representable and `Option.None` otherwise. ```silk import silk.option { Option } @@ -1057,7 +1057,7 @@ happen to have equal width. **Diagnostics:** A trapping conversion with statically compatible source and destination types has no compile-time range diagnostic for a dynamic value. An out-of-range execution traps at the -conversion operation. A checked conversion returns `None` rather than reporting a diagnostic or +conversion operation. A checked conversion returns `Option.None` rather than reporting a diagnostic or typed failure. **Evidence:** [integer scalar specification](../../../../openspec/specs/bootstrap-integer-scalars/spec.md), diff --git a/apps/docs/content/reference/style-guide.md b/apps/docs/content/reference/style-guide.md index 6a49bfd10..1c494fc56 100644 --- a/apps/docs/content/reference/style-guide.md +++ b/apps/docs/content/reference/style-guide.md @@ -34,9 +34,9 @@ filesystem.NotFoundError filesystem.PermissionDeniedError ``` -Use `Failure` for an unsuccessful outcome that carries an error, not for the error payload type -itself. For example, `NotFoundError` names a value while `Failure` names an outcome -containing that value. +Use `Failure` for an unsuccessful outcome variant that carries an error, not for the error payload +type itself. For example, `NotFoundError` names a value while +`Result.Failure` selects an outcome containing that value. **Boundary:** The suffix communicates API intent only. `NotFoundError` remains an ordinary value type and may be stored, passed, returned, or inspected outside an Effect. Conversely, every valid @@ -51,7 +51,7 @@ failure type. Such tooling must not imply that the name changes the type's seman **Current standard library:** Consistent. Public error payloads include `FileError`, `LogError`, `ProcessError`, `ParseError`, `HostInputError`, `StreamReadError`, `StreamWriteError`, -`OutOfMemoryError`, `StalledError`, and `TaskIdExhaustedError`. The `Failure` result outcome +`OutOfMemoryError`, `StalledError`, and `TaskIdExhaustedError`. The `Result.Failure` variant keeps its existing name because it is ordinary result data rather than an error declaration, and the fiber `Cancelled` outcome keeps its name for the same reason: it is the third arm of `Outcome` alongside `Success` and `Failure`, not an error payload type. diff --git a/apps/docs/content/reference/values-and-types.md b/apps/docs/content/reference/values-and-types.md index 60200c44c..f5f7f4372 100644 --- a/apps/docs/content/reference/values-and-types.md +++ b/apps/docs/content/reference/values-and-types.md @@ -277,15 +277,16 @@ fn scalarNumber(value: char) -> u32 { } ``` -`fromU32` returns `Some` for `0...0xd7ff` and `0xe000...0x10ffff`. It returns -`None` for surrogate values and larger integers, without truncating or trapping. `toU32` is total +`fromU32` returns `Option.Some` for `0...0xd7ff` and `0xe000...0x10ffff`. It returns +`Option.None` for surrogate values and larger integers, without truncating or trapping. `toU32` is total because every existing `char` is already a valid scalar. Canonical string traversal returns `char`; callers choose `toU32` explicitly when they need its integer value. **Diagnostics:** A literal containing zero or multiple scalar values reports `LEX0007`. Malformed escapes and invalid scalar spellings receive their literal diagnostic without constructing a partial `char`. Supplying `u32` where `char` is required, or `char` where `u32` is required, uses -the ordinary type-mismatch diagnostic; `fromU32` represents an invalid integer as `None` rather +the ordinary type-mismatch diagnostic; `fromU32` represents an invalid integer as +`Option.None` rather than a diagnostic or trap. **Evidence:** [character literal specification](../../../../openspec/specs/bootstrap-lexer/spec.md), @@ -556,6 +557,173 @@ memory cannot request Copy merely because its physical representation contains a **Evidence:** [owned value classification](ownership-and-borrowing.md#own-001--every-value-type-is-either-copy-or-affine). +## Nominal union values + +### NUNION-001 — A `union` declaration creates one nominal tagged sum + +**Status:** Confirmed + +`union Name { ... }` declares one nonempty, source-ordered set of variants under a single nominal +parent type. A variant is either a unit variant or a named-field variant with at least one field. +Generic parameters belong to the parent and are available in every variant field. + +```silk +union Option { + Some { value: T }, + None +} + +union HttpErrorCode { + DNSTimeout, + DNSError { rcode: Option, infoCode: Option } +} +``` + +`Option` is the value type. `Option.Some` and `Option.None` are constructors and +pattern selectors, not detached types or structural-union members. Two union declarations remain +different types even when their variants have identical names and fields. + +A nominal union is distinct from both other sum forms: + +- `enum Status { Ready, Waiting }` is a scalar enum: its members carry no payload and it has a + fixed-width integer representation. +- `A | B` is a structural union of already complete types: its members have independent identities + and the set normalizes without a declaring parent. + +**Boundary:** Named-field variants cannot use `{}`; use a unit variant instead. Variants have no +independent generic parameters, explicit discriminants, or standalone type identity. Raw C unions +and external linkage are outside this declaration form. + +**Diagnostics:** An empty union reports `SEM0165`; duplicate variants report `SEM0166`; an empty +named-field variant reports `PAR0026`. Invalid variant fields preserve declaration facts for +tooling but make the complete parent unavailable for execution. + +**Evidence:** [nominal union specification](../../../../openspec/changes/add-nominal-unions/specs/nominal-unions/spec.md), +[declaration tests](../../../../packages/compiler/test/DeclarationIndex.test.ts). + +### NUNION-002 — Construction selects a qualified variant of one complete parent + +**Status:** Confirmed + +A constructor first resolves its nominal parent and any explicit parent-argument prefix, then uses +only the selected variant's field initializers to infer the remaining arguments. + +```silk +union Result { + Success { value: A }, + Failure { error: E } +} + +fn succeed() -> Result { + return Result.Success { value: 42 } +} + +fn fail(error: string) -> Result { + return Result.Failure { error: move error } +} +``` + +`Result.Success` fixes `A` and cannot infer `E` from a field the `Success` variant does not +have; the declared result type supplies no implicit inference. The success constructor therefore +writes both arguments. The failure constructor writes `A` and infers `E` from its `error` field. + +Every named field must be initialized exactly once. Initializers evaluate in source order and are +stored in declaration order. A unit variant has no initializer body. Parent and field visibility +use the same module rules as structs: a private required field prevents raw construction outside +the defining module without disclosing hidden field details. + +**Boundary:** A parent value has no directly projectable fields, even when every variant declares a +same-spelled field. Bind a selected variant before using its payload. Construction never creates a +variant subtype and never flattens the parent into `A | B`. + +**Evidence:** [constructor and visibility tests](../../../../packages/compiler/test/StructValues.test.ts), +[generic inference rules](../../../../openspec/changes/add-nominal-unions/specs/generic-inference/spec.md). + +### NUNION-003 — Patterns select variants hierarchically and exhaustively + +**Status:** Confirmed + +Variant patterns spell the fully applied parent and then the variant. Named fields bind like struct +fields; unit variants have no payload pattern. + +```silk +fn unwrap(option: Option) -> i32 { + return match move option { + Option.Some { value } => value + Option.None => 0 + } +} +``` + +When a nominal union is itself a member of `A | B`, matching keeps both levels. A direct variant arm +first selects the structural member, then the nominal variant: + +```silk +struct OutOfMemoryError {} + +fn classify(error: HttpErrorCode | OutOfMemoryError) -> i32 { + return match move error { + HttpErrorCode.DNSError { rcode: _, infoCode: _ } => 2 + HttpErrorCode.DNSTimeout => 1 + OutOfMemoryError other => 0 + } +} +``` + +Exhaustiveness retains the complete applied parent identity. `Option` and `Option` have +distinct variant leaves when both occur in a structural union. A declared variant with a `never` +payload remains a required coverage leaf even though source cannot construct it. + +**Boundary:** Pattern arguments are explicit; they are not inferred from the scrutinee. A guarded +move remains provisional until the guard succeeds, so a false guard leaves the complete payload +available to later arms. + +**Evidence:** [hierarchical match tests](../../../../packages/compiler/test/StructValues.test.ts), +[pattern rules](patterns-and-destructuring.md). + +### NUNION-004 — Ownership and represented fields follow aggregate rules per active variant + +**Status:** Confirmed + +A nominal union is affine by default, even if every payload is Copy. `impl Copy` is admitted only +when every specialized field in every variant is Copy and no cleanup behavior conflicts. `impl +Drop`, interfaces, and operators target the complete parent type, never an individual variant. + +Moving a variant binding transfers the selected payload. Borrowed patterns preserve the parent +owner. Cleanup dispatches on the private active tag and releases only initialized fields of that +variant, in the ordinary aggregate order, exactly once. + +Callable- and Effect-bounded generic fields use the same exact represented-storage rules as struct +fields. Their environment, runner, access, suspension, and cleanup facts exist only for the variant +that declares the field; an inactive variant has no speculative payload to evaluate or release. + +**Boundary:** All-Copy fields do not imply Copy for the parent. Extracting one owned represented +field as an arbitrary partial move remains invalid; consume it through a variant pattern whose +ownership accounts for the rest of the active payload. + +**Evidence:** [ownership tests](../../../../packages/compiler/test/Ownership.test.ts), +[represented variant tests](../../../../packages/compiler/test/StructValues.test.ts), +[active cleanup tests](../../../../packages/compiler/test/BoxHeapIndirection.test.ts). + +### NUNION-005 — Tag and payload layout are private implementation facts + +**Status:** Confirmed + +Each concrete nominal-union application has one target layout containing a private tag and enough +aligned payload storage for its largest variant. Source order determines private variant ordinals. +Generic applications receive concrete layouts only when reachable and fully specialized. + +No source operation observes a tag value, payload offset, padding, or ABI choice. Construction, +calls, returns, matching, copying, and cleanup all use the compiler's verified calling shape. Inline +cycles across structs and unions are rejected unless source names an explicit finite indirection. + +**Boundary:** Layout equivalence does not create type compatibility, serialization stability, a C +ABI, or permission to reinterpret values. A backend cannot invent a fallback tag or offset that is +absent from the verified layout plan. + +**Evidence:** [layout tests](../../../../packages/compiler/test/Layout.test.ts), +[MIR verification](../../../../packages/compiler/src/MirVerification.ts). + ## Scalar enum values ### ENUM-001 — A scalar enum declares one closed nominal member set diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 221c84be0..1283f79bd 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -13,7 +13,7 @@ - [x] 2.2 Collect unions in the ordinary cross-kind module namespace with parent parameters and source-ordered variants before bodies, and verify forward declarations, duplicates, empty unions, and cross-kind collisions in declaration-index tests. - [x] 2.3 Resolve every variant field type, visibility exposure, generic reference, and inline aggregate dependency before body analysis, and verify invalid fields preserve sibling facts while making the complete parent non-executable. - [x] 2.4 Encode union declarations in deterministic module semantic surfaces, and verify encode/decode, equality, and dependency invalidation respond to variant order, kind, field, type, visibility, bound, and availability changes but ignore body-only edits. -- [ ] 2.5 Extend semantic occurrence, navigation, completion, documentation, and Analysis facade queries for parent, variant, and field identities, and verify go-to-definition/reference tests use canonical facts rather than syntax reconstruction. +- [x] 2.5 Extend semantic occurrence, navigation, completion, documentation, and Analysis facade queries for parent, variant, and field identities, and verify go-to-definition/reference tests use canonical facts rather than syntax reconstruction. ## 3. Type Application and Variant Construction @@ -37,10 +37,10 @@ ## 5. Ownership, Represented Fields, and Cleanup - [x] 5.1 Apply affine-by-default ownership and explicit Copy validation across every specialized variant field, and verify all-Copy payloads remain affine without `impl Copy` while one affine field rejects the implementation. -- [ ] 5.2 Build active-variant cleanup plans that reuse nominal Drop ordering and clean only initialized fields of the selected variant, and verify success, typed-failure, and ordinary scope exits release each owned payload exactly once. -- [ ] 5.3 Implement moved and borrowed variant-pattern ownership, including branch-local cleanup of omitted fields and rejection of invalid partial moves, and verify extracted and omitted fields have one final owner. -- [ ] 5.4 Realize callable-bounded fields only inside the active variant using exact static callable storage and access rules, and verify unsupported representations retain the pre-MIR storage fence. -- [ ] 5.5 Realize Effect-bounded fields only inside the active variant with lazy runner, environment, suspension, access, and cleanup facts, and verify unsupported shapes retain the pre-MIR storage fence. +- [x] 5.2 Build active-variant cleanup plans that reuse nominal Drop ordering and clean only initialized fields of the selected variant, and verify success, typed-failure, and ordinary scope exits release each owned payload exactly once. +- [x] 5.3 Implement moved and borrowed variant-pattern ownership, including branch-local cleanup of omitted fields and rejection of invalid partial moves, and verify extracted and omitted fields have one final owner. +- [x] 5.4 Realize callable-bounded fields only inside the active variant using exact static callable storage and access rules, and verify unsupported representations retain the pre-MIR storage fence. +- [x] 5.5 Realize Effect-bounded fields only inside the active variant with lazy runner, environment, suspension, access, and cleanup facts, and verify unsupported shapes retain the pre-MIR storage fence. ## 6. Target Layout and Calling Shapes @@ -52,18 +52,18 @@ ## 7. HIR, MIR, and Verification -- [ ] 7.1 Add explicit HIR construction and variant-selection nodes carrying applied parent, canonical variant, specialized fields, source mapping, access, selection path, and cleanup identity, and verify HIR snapshots retain both outer and inner selections. -- [ ] 7.2 Lower union construction and hierarchical patterns through the verified layout and ownership plans, and verify a direct nested arm produces an outer structural decision followed by the nominal variant decision. -- [ ] 7.3 Add monomorphic MIR operations for nominal construction, tag selection, dominated payload projection, and active copy/drop dispatch, and verify MIR rejects foreign parents, fields, layouts, tags, and inactive cleanup. -- [ ] 7.4 Extend MIR verification with selection-dominance and hierarchical-coverage checks, and verify an incomplete path or backend-default fallback is rejected before execution. -- [ ] 7.5 Add deterministic nominal-union MIR encoding and committed in-process goldens, and verify equivalent discovery traversals produce identical instance, variant, field, path, layout, and cleanup ordering. +- [x] 7.1 Add explicit HIR construction and variant-selection nodes carrying applied parent, canonical variant, specialized fields, source mapping, access, selection path, and cleanup identity, and verify HIR snapshots retain both outer and inner selections. +- [x] 7.2 Lower union construction and hierarchical patterns through the verified layout and ownership plans, and verify a direct nested arm produces an outer structural decision followed by the nominal variant decision. +- [x] 7.3 Add monomorphic MIR operations for nominal construction, tag selection, dominated payload projection, and active copy/drop dispatch, and verify MIR rejects foreign parents, fields, layouts, tags, and inactive cleanup. +- [x] 7.4 Extend MIR verification with selection-dominance and hierarchical-coverage checks, and verify an incomplete path or backend-default fallback is rejected before execution. +- [x] 7.5 Add deterministic nominal-union MIR encoding and committed in-process goldens, and verify equivalent discovery traversals produce identical instance, variant, field, path, layout, and cleanup ordering. ## 8. Evaluation and Backends -- [ ] 8.1 Represent evaluator values by semantic parent, active variant, and complete payload, and verify construction, movement, matching, storage, calls, returns, and active cleanup without evaluating inactive storage. -- [ ] 8.2 Implement direct WebAssembly nominal-union construction, transport, nested tag dispatch, payload mapping, and active cleanup from verified MIR/layout plans, and verify focused Wasm tests cover codegen-specific representation claims. -- [ ] 8.3 Implement native LLVM nominal-union construction, transport, nested tag dispatch, payload mapping, and active cleanup from the same plans, and verify target-specific lowering tests contain no backend-owned tag or offset decisions. -- [ ] 8.4 Add representative unit, payload, generic, represented-field, structural-root, and cleanup programs to the shared evaluator/Wasm assertions and native differential corpus, and verify all engines agree without adding per-feature native-agreement tests. +- [x] 8.1 Represent evaluator values by semantic parent, active variant, and complete payload, and verify construction, movement, matching, storage, calls, returns, and active cleanup without evaluating inactive storage. +- [x] 8.2 Implement direct WebAssembly nominal-union construction, transport, nested tag dispatch, payload mapping, and active cleanup from verified MIR/layout plans, and verify focused Wasm tests cover codegen-specific representation claims. +- [x] 8.3 Implement native LLVM nominal-union construction, transport, nested tag dispatch, payload mapping, and active cleanup from the same plans, and verify target-specific lowering tests contain no backend-owned tag or offset decisions. +- [x] 8.4 Add representative unit, payload, generic, represented-field, structural-root, and cleanup programs to the shared evaluator/Wasm assertions and native differential corpus, and verify all engines agree without adding per-feature native-agreement tests. ## 9. Carrier-Neutral Intrinsic Migration @@ -85,17 +85,17 @@ ## 11. Tooling, Documentation, and Acceptance -- [ ] 11.1 Extend syntax highlighting/token consumers, hover, completion, signature help, rename, references, and inspector/labs projections for union declarations and qualified variants, and verify LSP and tooling snapshots navigate through canonical parent/variant/field facts. -- [ ] 11.2 Add the prescriptive nominal-union reference documentation covering declaration syntax, generic qualification/inference, visibility, ownership, layout abstraction, matching, and distinction from `enum` and `A | B`, and verify documentation examples compile as doctests where supported. -- [ ] 11.3 Rewrite Option, Result, Effect, integer, and error-model documentation/examples for qualified direct variants and structural error composition, and verify no documentation search finds detached member types or wrapper `.value` matches. -- [ ] 11.4 Add or update acceptance corpus cases for `Result`, direct hierarchical matching, generic variants, Copy/Drop, recursion rejection, represented fields, and diagnostics, and verify each claim is tested at the cheapest policy-approved tier. +- [x] 11.1 Extend syntax highlighting/token consumers, hover, completion, signature help, rename, references, and inspector/labs projections for union declarations and qualified variants, and verify LSP and tooling snapshots navigate through canonical parent/variant/field facts. +- [x] 11.2 Add the prescriptive nominal-union reference documentation covering declaration syntax, generic qualification/inference, visibility, ownership, layout abstraction, matching, and distinction from `enum` and `A | B`, and verify documentation examples compile as doctests where supported. +- [x] 11.3 Rewrite Option, Result, Effect, integer, and error-model documentation/examples for qualified direct variants and structural error composition, and verify no documentation search finds detached member types or wrapper `.value` matches. +- [x] 11.4 Add or update acceptance corpus cases for `Result`, direct hierarchical matching, generic variants, Copy/Drop, recursion rejection, represented fields, and diagnostics, and verify each claim is tested at the cheapest policy-approved tier. ## 12. Final Verification - [ ] 12.1 Run the focused lexer, parser, formatter, declaration, semantic, matching, ownership, layout, HIR, MIR, evaluator, Wasm, native-corpus, intrinsic, stdlib, LSP, and doctest suites and verify every delta-spec scenario has direct evidence. -- [ ] 12.2 Run `pnpm typecheck` and fix every introduced type error, recording any unrelated pre-existing failure exactly. -- [ ] 12.3 Run `pnpm exec biome check .` and fix every introduced formatting or lint failure, recording any unrelated pre-existing failure exactly. +- [x] 12.2 Run `pnpm typecheck` and fix every introduced type error, recording any unrelated pre-existing failure exactly. +- [x] 12.3 Run `pnpm exec biome check .` and fix every introduced formatting or lint failure, recording any unrelated pre-existing failure exactly. - [ ] 12.4 Run `pnpm test` and fix every introduced test failure, recording any unrelated pre-existing failure exactly. - [ ] 12.5 Run `pnpm check` and verify the repository-wide required gate completes, or report the exact pre-existing blocker without describing the change as complete. - [ ] 12.6 Run `pnpm release:candidate` because compiler package contents change, and verify package contents, exports, stdlib embeddings, and release artifacts are internally consistent. -- [ ] 12.7 Run `openspec validate add-nominal-unions --strict` and verify proposal, all delta specs, design, and tasks remain coherent after implementation discoveries. +- [x] 12.7 Run `openspec validate add-nominal-unions --strict` and verify proposal, all delta specs, design, and tasks remain coherent after implementation discoveries. diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index aa07696a6..cbaeb02de 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -1391,6 +1391,26 @@ export const resolveUnionVariantTarget = ( ) return unavailableUnionVariantTarget(diagnostic, analyzed.diagnostics) } + const fullyResolved = DeclarationResolution.resolveTypeFact( + resolution.index, + source.id, + analyzed.fact, + (module, argumentPath) => + NameResolution.resolveType(nameResolution, resolution.index, module, argumentPath), + ) + if ( + fullyResolved.fact._tag === 'Resolved' && + Type.isNominal(fullyResolved.fact.type) && + fullyResolved.fact.type.module === base.module && + fullyResolved.fact.type.name === base.name + ) + return selectedUnionVariant( + declaration, + fullyResolved.fact.type, + spelling(source, variantToken), + variantToken, + Diagnostic.merge(analyzed.diagnostics, fullyResolved.diagnostics), + ) const supplied = applied?.arguments ?? [] const sourceParameters = declaration.typeParameters.filter( (parameter) => @@ -2961,7 +2981,10 @@ export const analyzeAggregateLiteral = ( for (const [parameterKey, inferred] of inferredArguments) currentSubstitution.set(parameterKey, inferred.argument) const candidateSubstitution = new Map(currentSubstitution) - if (!TypeInference.infer(expectedType, expression.type, candidateSubstitution)) { + if ( + !TypeInference.infer(expectedType, expression.type, candidateSubstitution) && + !typesCompatible(actualValue, Type.substitute(expectedType, currentSubstitution)) + ) { const impliedSubstitution = new Map() if (TypeInference.infer(expectedType, expression.type, impliedSubstitution)) { for (const parameter of aggregate.typeParameters) { diff --git a/packages/compiler/src/InstanceDiagnostics.ts b/packages/compiler/src/InstanceDiagnostics.ts index b6fd7d8c2..41563ced2 100644 --- a/packages/compiler/src/InstanceDiagnostics.ts +++ b/packages/compiler/src/InstanceDiagnostics.ts @@ -53,7 +53,8 @@ export const storedRepresentation = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return undefined + if (declaration?._tag !== 'StructDeclaration' && declaration?._tag !== 'UnionDeclaration') + return undefined const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), @@ -62,9 +63,22 @@ export const storedRepresentation = ( const fieldIndex = RepresentationField.resolveFields(index, [type]) const plans = RepresentationField.plansOf(index, type) const next = new Set(seen).add(typeKey) - for (const [ordinal, field] of declaration.fields.entries()) { + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields.map((field) => Object.freeze({ field, prefix: Object.freeze([]) })) + : declaration.variants.flatMap((variant) => + variant.fields.map((field) => + Object.freeze({ + field, + prefix: Object.freeze(variant.name._tag === 'Present' ? [variant.name.spelling] : []), + }), + ), + ) + for (const { field, prefix } of fields) { if (field.declaredType._tag !== 'Resolved' || field.name._tag !== 'Present') continue - const fieldPlans = plans.filter((candidate) => candidate.id.ordinal === ordinal) + const fieldPlans = plans.filter((candidate) => + RepresentationField.belongsTo(candidate.id, field.id), + ) for (const plan of fieldPlans) { const resolution = RepresentationField.lookup(fieldIndex, type, plan.id) if (resolution !== undefined) { @@ -77,7 +91,7 @@ export const storedRepresentation = ( (kind === 'Effect' && Type.isEffect(contract)) ) return Object.freeze({ - path: Object.freeze([field.name.spelling]), + path: Object.freeze([...prefix, field.name.spelling]), contract, open: resolution._tag === 'UnavailableRepresentationField', }) @@ -91,7 +105,7 @@ export const storedRepresentation = ( ) if (nested !== undefined) return Object.freeze({ - path: Object.freeze([field.name.spelling, ...nested.path]), + path: Object.freeze([...prefix, field.name.spelling, ...nested.path]), contract: nested.contract, open: nested.open, }) @@ -124,13 +138,17 @@ const collectNominals = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return + if (declaration?._tag !== 'StructDeclaration' && declaration?._tag !== 'UnionDeclaration') return const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), type.arguments, ) ?? new Map() - for (const field of declaration.fields) { + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields + : declaration.variants.flatMap((variant) => variant.fields) + for (const field of fields) { if (field.declaredType._tag !== 'Resolved') continue collectNominals(index, Type.substitute(field.declaredType.type, substitution), into, seen) } diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index e045bfed5..3224c3988 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -1129,6 +1129,53 @@ export const catalog = ( completed.set(key, entry) return entry } + const layoutAggregateField = ( + fieldType: DeclarationFacts.SemanticType, + fieldId: DeclarationFacts.FieldId, + ): CatalogEntry => { + const representationPlans = RepresentationField.plansOf(index, type).filter((plan) => + RepresentationField.belongsTo(plan.id, fieldId), + ) + let representationOrdinal = 0 + const visit = (candidate: DeclarationFacts.SemanticType): CatalogEntry => { + if (Type.isRepresented(candidate)) { + const plan = representationPlans.at(representationOrdinal) + representationOrdinal += 1 + const realization = + plan === undefined || callableRealizations === undefined + ? undefined + : FieldRealization.realizationOf(callableRealizations, type, plan.id) + if (realization === undefined) { + return unavailable(candidate, Object.freeze(Type.nominals(candidate)), { + _tag: 'InvalidDeclaration', + detail: 'represented executable values remain unavailable to layout', + }) + } + return FieldRealization.isCallableRealization(realization) + ? layoutRepresentedCallable(candidate, realization) + : layoutRepresentedEffect(candidate, realization) + } + if (Type.isFixedArray(candidate)) { + const element = visit(candidate.element) + if (element._tag === 'UnavailableLayoutEntry') return element + return ( + repeatedEntry(candidate, element) ?? + unavailable(candidate, Object.freeze(Type.nominals(candidate.element)), { + _tag: 'InvalidDeclaration', + detail: `array layout overflows for ${Type.encode(candidate)}`, + }) + ) + } + if (Type.isSlice(candidate)) { + const element = visit(candidate.element) + return element._tag === 'UnavailableLayoutEntry' + ? element + : sliceEntry(target, candidate, element) + } + return layoutType(candidate) + } + return visit(fieldType) + } const unionDeclaration = unionByType.get(`${type.module}\u0000${type.name}`) if (unionDeclaration !== undefined) { const union = unionDeclaration.union @@ -1210,7 +1257,7 @@ export const catalog = ( break } const fieldType = Type.substitute(field.declaredType.type, substitution) - const fieldLayout = layoutType(fieldType) + const fieldLayout = layoutAggregateField(fieldType, field.id) if (fieldLayout._tag === 'UnavailableLayoutEntry') { failure = unavailable( type, @@ -1358,48 +1405,7 @@ export const catalog = ( break } const fieldType = Type.substitute(field.declaredType.type, substitution) - const representationPlans = RepresentationField.plansOf(index, type).filter( - (plan) => plan.id.ordinal === field.id.ordinal, - ) - let representationOrdinal = 0 - const layoutFieldType = (candidate: DeclarationFacts.SemanticType): CatalogEntry => { - if (Type.isRepresented(candidate)) { - const plan = representationPlans.at(representationOrdinal) - representationOrdinal += 1 - const realization = - plan === undefined || callableRealizations === undefined - ? undefined - : FieldRealization.realizationOf(callableRealizations, type, plan.id) - if (realization === undefined) { - return unavailable(candidate, Object.freeze(Type.nominals(candidate)), { - _tag: 'InvalidDeclaration', - detail: 'represented executable values remain unavailable to layout', - }) - } - return FieldRealization.isCallableRealization(realization) - ? layoutRepresentedCallable(candidate, realization) - : layoutRepresentedEffect(candidate, realization) - } - if (Type.isFixedArray(candidate)) { - const element = layoutFieldType(candidate.element) - if (element._tag === 'UnavailableLayoutEntry') return element - return ( - repeatedEntry(candidate, element) ?? - unavailable(candidate, Object.freeze(Type.nominals(candidate.element)), { - _tag: 'InvalidDeclaration', - detail: `array layout overflows for ${Type.encode(candidate)}`, - }) - ) - } - if (Type.isSlice(candidate)) { - const element = layoutFieldType(candidate.element) - return element._tag === 'UnavailableLayoutEntry' - ? element - : sliceEntry(target, candidate, element) - } - return layoutType(candidate) - } - const fieldLayout = layoutFieldType(fieldType) + const fieldLayout = layoutAggregateField(fieldType, field.id) if (fieldLayout._tag === 'UnavailableLayoutEntry') { failure = unavailable( type, diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index bc34552a4..89aa50c29 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -45,6 +45,7 @@ import { topologicalRegions, typeText, } from './Mir.js' +import * as RepresentationField from './RepresentationField.js' import * as Scalar from './Scalar.js' import type * as SourceSpan from './SourceSpan.js' import type { @@ -4554,7 +4555,10 @@ export const verify = (self: Module): ReadonlyArray => { arm.member === undefined ? undefined : coverageFieldPathType(self.layout, arm.member, entry.path) - return selected !== undefined && SilkType.equals(selected, entry.cleanup.type) + return ( + selected !== undefined && + cleanupMatchesSemanticType(self.layout, entry.cleanup, selected) + ) }) : arm.selected.cleanup.length === 0) if (!cleanupValid) { @@ -4770,7 +4774,7 @@ export const verify = (self: Module): ReadonlyArray => { valueType.target, field.stored.realization.target, ) && - field.stored.realization.field.ordinal === field.field.ordinal && + RepresentationField.belongsTo(field.stored.realization.field, field.field) && field.stored.realization.instance.module === operation.type.type.module && field.stored.realization.instance.name === operation.type.type.name const storedEffectValid = @@ -4786,7 +4790,7 @@ export const verify = (self: Module): ReadonlyArray => { .module === field.stored.realization.runner.module && Hir.effectRunnerId(valueType.environment.instance.declaration, valueType.site) .name === field.stored.realization.runner.name && - field.stored.realization.field.ordinal === field.field.ordinal && + RepresentationField.belongsTo(field.stored.realization.field, field.field) && field.stored.realization.instance.module === operation.type.type.module && field.stored.realization.instance.name === operation.type.type.name return ( @@ -4831,12 +4835,45 @@ export const verify = (self: Module): ReadonlyArray => { operation.fields.every((field, ordinal) => { const declared = expected.fields.at(ordinal) const valueType = fn.localTypes.at(field.value.ordinal) + const storedCallableValid = + field.stored?._tag === 'StoredCallableField' && + declared !== undefined && + valueType?._tag === 'CallableValue' && + SilkType.equals(field.stored.type, declared.type) && + TypeCompatibility.isCompatible( + TypeCompatibility.check(valueType.type, field.stored.realization.contract), + ) && + Hir.matchesCallableTargetIdentity( + valueType.target, + field.stored.realization.target, + ) && + RepresentationField.belongsTo(field.stored.realization.field, field.field) && + field.stored.realization.instance.module === operation.type.type.module && + field.stored.realization.instance.name === operation.type.type.name + const storedEffectValid = + field.stored?._tag === 'StoredEffectField' && + declared !== undefined && + valueType?._tag === 'EffectValue' && + SilkType.equals(field.stored.type, declared.type) && + TypeCompatibility.isCompatible( + TypeCompatibility.check(valueType.type, field.stored.realization.contract), + ) && + Hir.sameExecutableSite(valueType.site, field.stored.realization.site) && + Hir.effectRunnerId(valueType.environment.instance.declaration, valueType.site) + .module === field.stored.realization.runner.module && + Hir.effectRunnerId(valueType.environment.instance.declaration, valueType.site) + .name === field.stored.realization.runner.name && + RepresentationField.belongsTo(field.stored.realization.field, field.field) && + field.stored.realization.instance.module === operation.type.type.module && + field.stored.realization.instance.name === operation.type.type.name return ( declared !== undefined && DeclarationFacts.sameFieldId(declared.id, field.field) && valueType !== undefined && - field.stored === undefined && - SilkType.equals(semanticType(valueType), declared.type) + ((field.stored === undefined && + SilkType.equals(semanticType(valueType), declared.type)) || + storedCallableValid || + storedEffectValid) ) }) if (!valid) { diff --git a/packages/compiler/src/RepresentationField.ts b/packages/compiler/src/RepresentationField.ts index 8e2db29cf..c2a377578 100644 --- a/packages/compiler/src/RepresentationField.ts +++ b/packages/compiler/src/RepresentationField.ts @@ -8,6 +8,7 @@ import * as Type from './Type.js' export interface Id { readonly _tag: 'RepresentedFieldId' readonly nominal: DeclarationFacts.CanonicalId + readonly variantOrdinal?: number readonly ordinal: number readonly useOrdinal: number } @@ -78,17 +79,26 @@ export const makeId = ( nominal: DeclarationFacts.CanonicalId, ordinal: number, useOrdinal: number, + variantOrdinal?: number, ): Id => Object.freeze({ _tag: 'RepresentedFieldId', nominal: Object.freeze({ ...nominal }), + ...(variantOrdinal === undefined ? {} : { variantOrdinal }), ordinal, useOrdinal, }) /** Canonical identity key; source provenance is intentionally absent. */ export const idKey = (self: Id): string => - `${self.nominal.module}.${self.nominal.name}:field:${self.ordinal}:representation:${self.useOrdinal}` + `${self.nominal.module}.${self.nominal.name}${self.variantOrdinal === undefined ? '' : `:variant:${self.variantOrdinal}`}:field:${self.ordinal}:representation:${self.useOrdinal}` + +/** Tests whether one represented-use identity belongs to a canonical aggregate field owner. */ +export const belongsTo = (self: Id, field: DeclarationFacts.FieldId): boolean => + self.ordinal === field.ordinal && + (field.owner._tag === 'StructFieldOwnerId' + ? self.variantOrdinal === undefined + : self.variantOrdinal === field.owner.variant.ordinal) /** Complete specialization key for one represented field. */ export const key = (instance: Type.Nominal, id: Id): string => `${Type.key(instance)}:${idKey(id)}` @@ -118,7 +128,7 @@ const plansOfInternal = ( const canonicalKey = `${canonical.module}.${canonical.name}` if (seen.has(canonicalKey)) return Object.freeze([]) const declaration = declarations.modules - .flatMap((module) => module.structs) + .flatMap((module) => [...module.structs, ...module.unions]) .find( (candidate) => candidate.canonical._tag === 'Canonical' && @@ -143,7 +153,7 @@ const plansOfInternal = ( if (Type.isUnion(type)) return Object.freeze(type.members.flatMap(symbolicUses)) if (!Type.isNominal(type) || Type.isIntrinsicNominal(type)) return Object.freeze([]) const nested = declarations.modules - .flatMap((module) => module.structs) + .flatMap((module) => [...module.structs, ...module.unions]) .find( (candidate) => candidate.canonical._tag === 'Canonical' && @@ -168,13 +178,24 @@ const plansOfInternal = ( }), ) } + const fields: ReadonlyArray<{ + readonly field: DeclarationFacts.FieldFact + readonly variantOrdinal?: number + }> = + declaration._tag === 'StructDeclaration' + ? declaration.fields.map((field) => Object.freeze({ field })) + : declaration.variants.flatMap((variant) => + variant.fields.map((field) => + Object.freeze({ field, variantOrdinal: variant.id.ordinal }), + ), + ) return Object.freeze( - declaration.fields.flatMap((field, ordinal): ReadonlyArray => { + fields.flatMap(({ field, variantOrdinal }): ReadonlyArray => { if (field.declaredType._tag !== 'Resolved') return [] return symbolicUses(field.declaredType.type).map((use, useOrdinal) => Object.freeze({ _tag: 'RepresentationFieldPlan' as const, - id: makeId(canonical, ordinal, useOrdinal), + id: makeId(canonical, field.id.ordinal, useOrdinal, variantOrdinal), parameter: use.parameter, requiredBound: use.requiredBound, }), @@ -191,14 +212,19 @@ export const plansOf = ( const provenanceOf = (declarations: DeclarationIndex.Index, plan: Plan): Provenance | undefined => { const declaration = declarations.modules - .flatMap((module) => module.structs) + .flatMap((module) => [...module.structs, ...module.unions]) .find( (candidate) => candidate.canonical._tag === 'Canonical' && candidate.canonical.id.module === plan.id.nominal.module && candidate.canonical.id.name === plan.id.nominal.name, ) - const field = declaration?.fields.at(plan.id.ordinal) + const field = + declaration?._tag === 'StructDeclaration' + ? declaration.fields.at(plan.id.ordinal) + : declaration?.variants + .find((variant) => variant.id.ordinal === plan.id.variantOrdinal) + ?.fields.at(plan.id.ordinal) const parameter = declaration?.typeParameters.find( (candidate) => Type.key(candidate.type) === Type.key(plan.parameter), ) @@ -208,7 +234,7 @@ const provenanceOf = (declarations: DeclarationIndex.Index, plan: Plan): Provena } const resolvePlan = ( - declaration: DeclarationFacts.StructFact, + declaration: DeclarationFacts.StructFact | DeclarationFacts.UnionFact, instance: Type.Nominal, plan: Plan, provenance: Provenance, @@ -269,7 +295,7 @@ export const resolveFields = ( const resolutions = new Map() for (const instance of instances) { const declaration = declarations.modules - .flatMap((module) => module.structs) + .flatMap((module) => [...module.structs, ...module.unions]) .find( (candidate) => candidate.canonical._tag === 'Canonical' && diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index 76d657735..2a412e807 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -96,7 +96,7 @@ export const modules = [ module: 'silk/effect', path: 'silk/effect.silk', sourceIdentity: 'silk/effect', - digest: '4ec13a79a9f01a85a700cc95b2e0171c8c395aeff65a9f4d96d39b53f2a4dede', + digest: 'ff1454c143be6d6c1406a0ad284d8c9a1d26d56ef3228d63318881f8cead95b3', documentation: 'silk/effect.silk', layer: 'portable', runtimeInventory: [ @@ -108,7 +108,7 @@ export const modules = [ ], namespace: 'Effect', source: - "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core catches typed\n// failures and binds typed requirements; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because conversion does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is converted into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", + "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core catches typed\n// failures and binds typed requirements; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because conversion does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\n///\n/// This is ordinary Silk composition: success is mapped into `Result.Success`, then `catchAll`\n/// maps the complete typed failure value into `Result.Failure`. Compound failure unions and\n/// requirements are preserved, and the exact `once fn` adapters transfer affine payloads once.\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is converted into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", }, { module: 'silk/execution', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 6955e3348..89fd40c5a 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'e7b8173a01f2114bff556737f27b35162b0d584948ad3b94a4ee7e51041a8e52' +export const compilerDigest = '9919088e2dcdacd025e3cb7a605714c7608bb226f5ddc9345c2ec1efe9962bca' diff --git a/packages/compiler/stdlib/silk/effect.silk b/packages/compiler/stdlib/silk/effect.silk index 922e9c8a7..c2615e32d 100644 --- a/packages/compiler/stdlib/silk/effect.silk +++ b/packages/compiler/stdlib/silk/effect.silk @@ -226,6 +226,10 @@ pub effect fn logError( /// } /// } /// ``` +/// +/// This is ordinary Silk composition: success is mapped into `Result.Success`, then `catchAll` +/// maps the complete typed failure value into `Result.Failure`. Compound failure unions and +/// requirements are preserved, and the exact `once fn` adapters transfer affine payloads once. pub effect fn result( protected: once Effect ) -> Result ? R { diff --git a/packages/compiler/test/DeclarationIndex.test.ts b/packages/compiler/test/DeclarationIndex.test.ts index abb9fd41e..cd9c725c1 100644 --- a/packages/compiler/test/DeclarationIndex.test.ts +++ b/packages/compiler/test/DeclarationIndex.test.ts @@ -1016,7 +1016,7 @@ struct Damaged {}` ) const duplicate = index.diagnostics.find((diagnostic) => diagnostic.code === 'SEM0165') assert.deepEqual( - duplicate?.relatedSpans.map((related) => source.slice(related.span.start, related.span.end)), + duplicate?.relatedSpans?.map((related) => source.slice(related.span.start, related.span.end)), ['Same'], ) assert.strictEqual( diff --git a/packages/compiler/test/EditorIntelligence.test.ts b/packages/compiler/test/EditorIntelligence.test.ts index 475afcb7a..b6d901f14 100644 --- a/packages/compiler/test/EditorIntelligence.test.ts +++ b/packages/compiler/test/EditorIntelligence.test.ts @@ -117,7 +117,7 @@ pub union Result { Failure { pub error: E }, } pub fn main() -> i32 { return 0 }` - return Analysis.ofSource('main', encoder.encode(source)).pipe( + return Analysis.ofSourceRealized('main', encoder.encode(source)).pipe( Effect.map((snapshot) => { const declaration = occurrenceAt(snapshot, source, 'Result') const variant = occurrenceAt(snapshot, source, 'Success') @@ -180,6 +180,33 @@ pub fn main() -> i32 { let state = State. return 0 }` ) }) +it.effect('navigates constructor and pattern variants through one canonical identity', () => { + const source = `union Option { Some { value: T }, None } +fn unwrap(option: Option) -> i32 { + return match move option { + Option.Some { value } => value + Option.None => 0 + } +} +pub fn main() -> i32 { return unwrap(Option.Some { value: 42 }) }` + return Analysis.ofSource('main', encoder.encode(source)).pipe( + Effect.map((snapshot) => { + const declaration = occurrenceAt(snapshot, source, 'Some') + const pattern = occurrenceAt(snapshot, source, 'Some', 1) + const construction = occurrenceAt(snapshot, source, 'Some', 2) + + assert.strictEqual(declaration?.role, 'Declaration') + assert.strictEqual(pattern?.role, 'Value') + assert.strictEqual(construction?.role, 'Value') + assert.deepEqual(pattern?.resolution, declaration?.resolution) + assert.deepEqual(construction?.resolution, declaration?.resolution) + assert.deepEqual(pattern?.declaration, declaration?.declaration) + assert.deepEqual(construction?.declaration, declaration?.declaration) + return undefined + }), + ) +}) + it.effect('answers raw documentation for modules, declarations, children, and references', () => { const source = `//! Recovery module. /// A recoverable problem. diff --git a/packages/compiler/test/Random.test.ts b/packages/compiler/test/Random.test.ts index 01141d7be..c81418e6a 100644 --- a/packages/compiler/test/Random.test.ts +++ b/packages/compiler/test/Random.test.ts @@ -284,9 +284,6 @@ it.effect('stages every evaluator failure before touching caller storage', () => writes.push(bytes) for (const [index, byte] of bytes.entries()) output[index] = byte }, - optionValue: () => { - throw new Error('unexpected option') - }, handleValue: () => { throw new Error('unexpected handle') }, @@ -325,9 +322,6 @@ it.effect('stages every evaluator failure before touching caller storage', () => writeByteView: () => { emptyByteWrites += 1 }, - optionValue: () => { - throw new Error('unexpected option') - }, handleValue: () => { throw new Error('unexpected handle') }, diff --git a/packages/compiler/test/RepresentationField.test.ts b/packages/compiler/test/RepresentationField.test.ts index fe1d3e6b1..63d57ff5a 100644 --- a/packages/compiler/test/RepresentationField.test.ts +++ b/packages/compiler/test/RepresentationField.test.ts @@ -227,6 +227,36 @@ struct Choice i32, G: fn(i32) -> i32> { operation: Left | Right }), ) +it.effect('keeps represented fields scoped to their nominal union variants', () => + Effect.gen(function* () { + const module = 'representation-field/nominal-union' + const index = yield* declarations( + module, + `union Choice i32> { + Left { operation: F }, + Right { operation: F }, +}`, + ) + const argument = exactCallable(module) + const instance = Type.nominal(module, 'Choice', [argument]) + const plans = RepresentationField.plansOf(index, instance) + const resolutions = RepresentationField.resolveFields(index, [instance]) + + assert.deepEqual( + plans.map((plan) => [plan.id.variantOrdinal, plan.id.ordinal, plan.id.useOrdinal]), + [ + [0, 0, 0], + [1, 0, 0], + ], + ) + assert.strictEqual(new Set(plans.map((plan) => RepresentationField.idKey(plan.id))).size, 2) + assert.deepEqual( + plans.map((plan) => RepresentationField.lookup(resolutions, instance, plan.id)?._tag), + ['ResolvedRepresentationField', 'ResolvedRepresentationField'], + ) + }), +) + it.effect('retains explicit open recovery facts after the source specialization gate', () => Effect.gen(function* () { const module = 'representation-field/open' diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 4e8c0717f..25fa77c8b 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -425,6 +425,61 @@ pub fn main() -> i32 { }), ) +it.effect('realizes callable and Effect fields only in their active nominal variant', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/represented-fields', + ascii(`union Parser i32> { Empty, Ready { parse: F } } +union Deferred> { Empty, Ready { operation: F } } + +fn increment(value: i32) -> i32 { return value + 1 } + +fn parse i32>(parser: Parser) -> i32 { + return match move parser { + Parser.Empty => 0 + Parser.Ready { parse } => parse(20) + } +} + +fn force>(deferred: Deferred) -> i32 { + return match move deferred { + Deferred.Empty => 0 + Deferred.Ready { operation } => run operation + } +} + +pub fn main() -> i32 { + let parser = Parser.Ready { parse: increment } + let deferred = Deferred.Ready { operation: effect { return 21 } } + return parse(move parser) + force(move deferred) +}`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + const constructions = Analysis.loweredMir(self) + .functions.flatMap(MirVerification.operations) + .filter((operation) => operation._tag === 'ConstructUnionVariant') + assert.deepEqual( + constructions.flatMap((operation) => + operation._tag === 'ConstructUnionVariant' + ? operation.fields.flatMap((field) => + field.stored === undefined ? [] : [field.stored._tag], + ) + : [], + ), + ['StoredCallableField', 'StoredEffectField'], + ) + assert.deepEqual(MirVerification.verify(Analysis.loweredMir(self)), []) + const outcome = Analysis.evaluate(self) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + it.effect('evaluates initializers in source order before constructing in declaration order', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/fixtures/intrinsic-inventory.json b/packages/compiler/test/fixtures/intrinsic-inventory.json index 6e01191a6..ac157a6a7 100644 --- a/packages/compiler/test/fixtures/intrinsic-inventory.json +++ b/packages/compiler/test/fixtures/intrinsic-inventory.json @@ -1,9 +1,5 @@ { - "targets": [ - "Evaluator", - "LLVM", - "Wasm" - ], + "targets": ["Evaluator", "LLVM", "Wasm"], "entries": [ { "operation": "Intrinsic.boolEquals", diff --git a/packages/compiler/test/support/corpus.ts b/packages/compiler/test/support/corpus.ts index 9b3eb4cd0..54b40762c 100644 --- a/packages/compiler/test/support/corpus.ts +++ b/packages/compiler/test/support/corpus.ts @@ -1058,6 +1058,73 @@ export const corpus: ReadonlyArray = [ source: scalarEnumLaneAcceptance, expected: { _tag: 'Completes', result: 42 }, }, + { + name: 'nominal-result-compound-error', + source: `struct Data { value: i32 } +union HttpErrorCode { DNSTimeout, DNSError { rcode: i32 } } +struct OutOfMemoryError {} +union Result { Success { value: A }, Failure { error: E } } + +fn inspect(result: Result) -> i32 { + return match move result { + Result.Success { value } => value.value + Result.Failure { error } => match move error { + HttpErrorCode.DNSTimeout => 1 + HttpErrorCode.DNSError { rcode } => rcode + OutOfMemoryError other => 0 + } + } +} + +pub fn main() -> i32 { + let success = Result.Success { + value: Data { value: 21 }, + } + let failure = Result.Failure { + error: HttpErrorCode.DNSError { rcode: 21 }, + } + return inspect(move success) + inspect(move failure) +}`, + expected: { _tag: 'Completes', result: 42 }, + }, + { + name: 'nominal-union-represented-copy-drop', + source: `union Parser i32> { Empty, Ready { parse: F } } +union Deferred> { Empty, Ready { operation: F } } +union Flag { Empty, Value { value: i32 } } +impl Copy for Flag {} +struct Token {} +impl Drop for Token { fn drop(self: &mut Token) -> () { return () } } +union Owner { Empty, Present { token: Token, value: i32 } } + +fn increment(value: i32) -> i32 { return value + 1 } +fn parse i32>(parser: Parser) -> i32 { + return match move parser { + Parser.Empty => 0 + Parser.Ready { parse } => parse(19) + } +} +fn force>(deferred: Deferred) -> i32 { + return match move deferred { + Deferred.Empty => 0 + Deferred.Ready { operation } => run operation + } +} +fn copyFlag(flag: Flag) -> i32 { + let copied = flag + return match move copied { Flag.Empty => 0 Flag.Value { value } => value } +} +fn consume(owner: Owner) -> i32 { + return match move owner { Owner.Empty => 0 Owner.Present { value, .. } => value } +} +pub fn main() -> i32 { + let parser = Parser.Ready { parse: increment } + let deferred = Deferred.Ready { operation: effect { return 18 } } + let owner = Owner.Present { token: Token {}, value: 2 } + return parse(move parser) + force(move deferred) + copyFlag(Flag.Value { value: 2 }) + consume(move owner) +}`, + expected: { _tag: 'Completes', result: 42 }, + }, { name: 'seeded-random-fingerprint', source: seededRandomFingerprint, diff --git a/packages/docgen/src/Document.ts b/packages/docgen/src/Document.ts index 70d2c4022..e3a034196 100644 --- a/packages/docgen/src/Document.ts +++ b/packages/docgen/src/Document.ts @@ -21,7 +21,15 @@ export interface LinkTarget { readonly id: string readonly module: string readonly name: string - readonly kind: 'Function' | 'Struct' | 'Service' | 'Interface' | 'Constant' | 'Role' + readonly kind: + | 'Function' + | 'Struct' + | 'Enum' + | 'Union' + | 'Service' + | 'Interface' + | 'Constant' + | 'Role' } export type Inline = diff --git a/packages/docgen/src/Model.ts b/packages/docgen/src/Model.ts index adc93e3df..beae01b65 100644 --- a/packages/docgen/src/Model.ts +++ b/packages/docgen/src/Model.ts @@ -16,7 +16,15 @@ export interface LinkTarget { readonly id: string readonly module: string readonly name: string - readonly kind: 'Function' | 'Struct' | 'Service' | 'Interface' | 'Constant' | 'Role' + readonly kind: + | 'Function' + | 'Struct' + | 'Enum' + | 'Union' + | 'Service' + | 'Interface' + | 'Constant' + | 'Role' } export type Inline = @@ -100,6 +108,8 @@ export type ItemKind = | 'Struct' | 'Enum' | 'EnumMember' + | 'Union' + | 'UnionVariant' | 'Service' | 'Interface' | 'Constant' @@ -190,6 +200,8 @@ const linkKindOf = (value: unknown): LinkTarget['kind'] | undefined => { switch (value) { case 'Function': case 'Struct': + case 'Enum': + case 'Union': case 'Service': case 'Interface': case 'Constant': @@ -405,6 +417,8 @@ const itemKindOf = (value: unknown): ItemKind | undefined => { case 'Struct': case 'Enum': case 'EnumMember': + case 'Union': + case 'UnionVariant': case 'Service': case 'Interface': case 'Constant': diff --git a/packages/docgen/src/Project.ts b/packages/docgen/src/Project.ts index cb7c30e1f..35ecbd960 100644 --- a/packages/docgen/src/Project.ts +++ b/packages/docgen/src/Project.ts @@ -11,6 +11,8 @@ export type ItemKind = | 'Struct' | 'Enum' | 'EnumMember' + | 'Union' + | 'UnionVariant' | 'Service' | 'Interface' | 'Constant' @@ -68,6 +70,10 @@ const linkTargetKind = (member: DeclarationFacts.MemberFact): Document.LinkTarge return 'Function' case 'StructDeclaration': return 'Struct' + case 'EnumDeclaration': + return 'Enum' + case 'UnionDeclaration': + return 'Union' case 'ServiceDeclaration': return 'Service' case 'InterfaceDeclaration': @@ -87,6 +93,8 @@ const itemKind = (member: DeclarationFacts.MemberFact): ItemKind => { return 'Struct' case 'EnumDeclaration': return 'Enum' + case 'UnionDeclaration': + return 'Union' case 'ServiceDeclaration': return 'Service' case 'InterfaceDeclaration': @@ -279,6 +287,38 @@ const enumMemberItem = ( }) } +const unionVariantItem = ( + snapshot: Analysis.FrontendSnapshot, + module: string, + source: SourceFile.SourceFile, + parent: string, + union: DeclarationFacts.UnionFact, + variant: DeclarationFacts.UnionVariantFact, + options: Options, +): Item => { + const name = nameOf(variant.name, '_') + const documentation = resolveDocumentation( + snapshot, + module, + parsedDocumentation(snapshot, module, source, variant.syntax), + ) + const id = `${parent}::variant:${variant.id.ordinal}` + return Object.freeze({ + id, + kind: 'UnionVariant', + name, + visibility: 'Inherited', + signature: Object.freeze({ text: Presentation.unionVariant(union, variant).text }), + source: rangeOf(variant.syntax), + ...(documentation === undefined ? {} : { documentation }), + children: Object.freeze( + variant.fields + .filter((field) => options.includePrivate === true || field.visibility === 'Public') + .map((field) => fieldItem(snapshot, module, source, id, field)), + ), + }) +} + const memberPresentation = (member: DeclarationFacts.MemberFact) => { switch (member._tag) { case 'FunctionDeclaration': @@ -287,6 +327,8 @@ const memberPresentation = (member: DeclarationFacts.MemberFact) => { return Presentation.structDeclaration(member) case 'EnumDeclaration': return Presentation.enumDeclaration(member) + case 'UnionDeclaration': + return Presentation.unionDeclaration(member) case 'ServiceDeclaration': case 'InterfaceDeclaration': return Presentation.serviceDeclaration(member) @@ -318,6 +360,10 @@ const ownedChildren = ( return member.members.map((enumMember) => enumMemberItem(snapshot, module, source, id, enumMember), ) + case 'UnionDeclaration': + return member.variants.map((variant) => + unionVariantItem(snapshot, module, source, id, member, variant, options), + ) case 'ServiceDeclaration': case 'InterfaceDeclaration': return member.operations.map((operation) => diff --git a/packages/docgen/test/Model.test.ts b/packages/docgen/test/Model.test.ts index 37472e90a..fac3b606c 100644 --- a/packages/docgen/test/Model.test.ts +++ b/packages/docgen/test/Model.test.ts @@ -47,8 +47,8 @@ it('decodes the canonical signature object and required declaration fields', () ) }) -it('decodes enum, enum-member, and role declaration kinds', () => { - for (const kind of ['Enum', 'EnumMember', 'Role']) { +it('decodes enum, union, child, and role declaration kinds', () => { + for (const kind of ['Enum', 'EnumMember', 'Union', 'UnionVariant', 'Role']) { assert.strictEqual(Model.decode(project(item({ kind })))._tag, 'Decoded', kind) } }) diff --git a/packages/docgen/test/Project.test.ts b/packages/docgen/test/Project.test.ts index 2e67a6462..5b3e5ca7e 100644 --- a/packages/docgen/test/Project.test.ts +++ b/packages/docgen/test/Project.test.ts @@ -40,6 +40,14 @@ pub enum State { /// The operation can start. Ready } + +/// A computation outcome. +pub union Outcome { + /// A successful payload. + Success { pub value: T }, + /// No payload is available. + Empty +} ` const snapshot = yield* Analysis.ofSource('project/main', encode(source)) const publicProject = Project.make(snapshot) @@ -48,7 +56,7 @@ pub enum State { assert.strictEqual(module.documentation?.markdown, 'Recovery utilities.') assert.deepStrictEqual( module.items.filter((item) => item.kind !== 'Implementation').map((item) => item.name), - ['defaultCode', 'Primary', 'recover', 'Problem', 'State'], + ['defaultCode', 'Primary', 'recover', 'Problem', 'State', 'Outcome'], ) const defaultCode = module.items.find((item) => item.name === 'defaultCode') assert.strictEqual(defaultCode?.kind, 'Constant') @@ -70,6 +78,19 @@ pub enum State { assert.strictEqual(state?.documentation?.markdown, 'One recovery state.') assert.strictEqual(state?.children.at(0)?.name, 'Ready') assert.strictEqual(state?.children.at(0)?.documentation?.markdown, 'The operation can start.') + const outcome = module.items.find((item) => item.name === 'Outcome') + assert.strictEqual(outcome?.kind, 'Union') + assert.strictEqual(outcome?.signature.text, 'pub union Outcome') + const variants = outcome?.children.filter((item) => item.kind === 'UnionVariant') + assert.deepEqual( + variants?.map((item) => [item.kind, item.name, item.children.length]), + [ + ['UnionVariant', 'Success', 1], + ['UnionVariant', 'Empty', 0], + ], + ) + assert.strictEqual(variants?.at(0)?.documentation?.markdown, 'A successful payload.') + assert.strictEqual(variants?.at(0)?.children.at(0)?.name, 'value') const privateProject = Project.make(snapshot, { includePrivate: true }) assert.isTrue(privateProject.modules[0]?.items.some((item) => item.name === 'helper')) diff --git a/packages/lsp/src/Document.ts b/packages/lsp/src/Document.ts index d3d799511..7db826192 100644 --- a/packages/lsp/src/Document.ts +++ b/packages/lsp/src/Document.ts @@ -1,6 +1,6 @@ import * as Analysis from '@silklang/compiler/Analysis' import type * as AutoImport from '@silklang/compiler/AutoImport' -import type * as DeclarationFacts from '@silklang/compiler/DeclarationFacts' +import * as DeclarationFacts from '@silklang/compiler/DeclarationFacts' import * as Diagnostic from '@silklang/compiler/Diagnostic' import * as FormattedDocument from '@silklang/compiler/FormattedDocument' import * as ImportPath from '@silklang/compiler/ImportPath' @@ -760,7 +760,7 @@ const enclosingStructLiteral = ( } | undefined const visit = (node: SyntaxTree.Node): void => { - if (node.kind === 'StructLiteralExpression') { + if (node.kind === 'StructLiteralExpression' || node.kind === 'UnionVariantExpression') { const target = node.children[0] const leftBrace = node.children.find( (child) => SyntaxTree.isToken(child) && child.kind === 'LeftBrace', @@ -821,10 +821,16 @@ export const signatureHelp = ( if (call === undefined) { const structLiteral = enclosingStructLiteral(syntax.root, offset) if (structLiteral === undefined) return undefined - const targetPath = - SyntaxTree.isNode(structLiteral.target) && structLiteral.target.kind === 'AppliedType' - ? (SyntaxTree.directNode(structLiteral.target, 'TypePath') ?? structLiteral.target) - : structLiteral.target + const variantSelector = + SyntaxTree.directNode(structLiteral.literal, 'UnionVariantSelector') ?? + (SyntaxTree.isNode(structLiteral.target) && + structLiteral.target.kind === 'UnionVariantSelector' + ? structLiteral.target + : undefined) + let targetPath = structLiteral.target + if (variantSelector !== undefined) targetPath = variantSelector + else if (SyntaxTree.isNode(targetPath) && targetPath.kind === 'AppliedType') + targetPath = SyntaxTree.directNode(targetPath, 'TypePath') ?? targetPath const occurrence = Analysis.semanticOccurrenceAt( snapshot, self.module, @@ -832,12 +838,48 @@ export const signatureHelp = ( ) if (occurrence?.resolution._tag !== 'Available') return undefined const identity = occurrence.resolution.identity - if (identity._tag !== 'DeclarationIdentity') return undefined - const declaration = Analysis.declarationForIdentity(snapshot, identity) - if (declaration?._tag !== 'StructDeclaration') return undefined - const fields = declaration.fields.filter( + const selected = (() => { + if (identity._tag === 'DeclarationIdentity') { + const declaration = Analysis.declarationForIdentity(snapshot, identity) + return declaration?._tag === 'StructDeclaration' + ? Object.freeze({ + fields: declaration.fields, + kind: 'Struct' as const, + presentation: Presentation.structDeclaration(declaration).text, + }) + : undefined + } + if (identity._tag !== 'UnionVariantIdentity') return undefined + const resolvedUnion = Analysis.unionByName( + snapshot, + identity.id.union.module, + identity.id.union.name, + ) + if (resolvedUnion._tag !== 'Resolved') return undefined + const resolvedVariant = Analysis.unionVariantByName( + resolvedUnion.declaration, + identity.id.name, + ) + return resolvedVariant._tag === 'Resolved' + ? Object.freeze({ + fields: resolvedVariant.variant.fields, + kind: 'UnionVariant' as const, + presentation: Presentation.unionVariant( + resolvedUnion.declaration, + resolvedVariant.variant, + ).text, + }) + : undefined + })() + if (selected === undefined) return undefined + const fields = selected.fields.filter( (field) => field.visibility === 'Public' || occurrence.declaration?.module === self.module, ) + const fieldList = fields.map((field) => Presentation.field(field).text).join(', ') + const label = + selected.kind === 'Struct' + ? `${selected.presentation} { ${fieldList} }` + : selected.presentation.replace(/\{[^}]*\}/, `{ ${fieldList} }`) const activeInitializer = structLiteral.initializers.find( (initializer) => offset >= initializer.span.start && offset <= initializer.span.end, ) @@ -853,7 +895,7 @@ export const signatureHelp = ( const activeField = fieldIdentity === undefined ? -1 - : fields.findIndex((field) => field.id.ordinal === fieldIdentity.id.ordinal) + : fields.findIndex((field) => DeclarationFacts.sameFieldId(field.id, fieldIdentity.id)) const precedingInitializers = structLiteral.initializers.filter( (initializer) => initializer.span.end < offset, ).length @@ -865,9 +907,7 @@ export const signatureHelp = ( return { signatures: [ { - label: `${Presentation.structDeclaration(declaration).text} { ${fields - .map((field) => Presentation.field(field).text) - .join(', ')} }`, + label, parameters: fields.map((field) => ({ label: Presentation.field(field).text })), ...(documentation === undefined || documentation.length === 0 ? {} @@ -1617,6 +1657,48 @@ export const symbols = ( }, ] } + if (member._tag === 'UnionDeclaration') { + const children = member.variants.flatMap((variant) => + variant.name._tag === 'Present' + ? [ + { + name: variant.name.spelling, + kind: SymbolKind.EnumMember, + range: LineIndex.rangeOf(self.index, SyntaxTree.span(variant.syntax)), + selectionRange: LineIndex.rangeOf(self.index, variant.name.token.span), + ...(variant.fields.length === 0 + ? {} + : { + children: variant.fields.flatMap((field) => + field.name._tag === 'Present' + ? [ + { + name: field.name.spelling, + kind: SymbolKind.Field, + range: LineIndex.rangeOf(self.index, SyntaxTree.span(field.syntax)), + selectionRange: LineIndex.rangeOf( + self.index, + field.name.token.span, + ), + }, + ] + : [], + ), + }), + }, + ] + : [], + ) + return [ + { + name: member.name.spelling, + kind: SymbolKind.Enum, + range, + selectionRange, + ...(children.length > 0 ? { children } : {}), + }, + ] + } const fields = member.fields.flatMap((field) => field.name._tag === 'Present' ? [ @@ -1849,6 +1931,8 @@ export const semanticTokens = ( const foldableKinds: ReadonlySet = new Set([ 'Block', 'StructDeclaration', + 'UnionDeclaration', + 'UnionVariant', 'ServiceDeclaration', 'InterfaceDeclaration', 'ImplDeclaration', diff --git a/packages/lsp/test/Document.test.ts b/packages/lsp/test/Document.test.ts index 764b11ce4..fe184e75d 100644 --- a/packages/lsp/test/Document.test.ts +++ b/packages/lsp/test/Document.test.ts @@ -12,7 +12,7 @@ import * as WorkspaceInventory from '@silklang/compiler/WorkspaceInventory' import * as Effect from 'effect/Effect' import * as Layer from 'effect/Layer' import * as Option from 'effect/Option' -import { SymbolKind } from 'vscode-languageserver-types' +import { CompletionItemKind, SymbolKind } from 'vscode-languageserver-types' import * as Document from '../src/Document.js' import * as EmbeddedFormatting from './fixtures/embeddedFormatting.js' @@ -680,6 +680,79 @@ pub fn main() -> i32 { }), ) +it.effect( + 'uses canonical nominal union identities for editor navigation and constructor help', + () => + Effect.gen(function* () { + const source = `union Option { Some { value: T }, None } +fn unwrap(option: Option) -> i32 { + return match move option { Option.Some { value } => value Option.None => 0 } +} +pub fn main() -> i32 { + let option = Option.Some { value: 42 } + return unwrap(move option) +}` + const { document, snapshot } = yield* open(source) + const someReference = positionOf(source, 'Some', 1) + const hover = Document.hover(document, snapshot, someReference) + assert.deepEqual(hover?.contents, { + kind: 'markdown', + value: '```silk\nOption.Some { value: T }: Option\n```', + }) + + const completionSource = `union Option { Some { value: T }, None } +pub fn main() -> i32 { let option = Option. return 0 }` + const completionState = yield* open(completionSource) + const completion = Document.completion( + completionState.document, + completionState.snapshot, + positionAt(completionSource, completionSource.indexOf('Option.') + 'Option.'.length), + ) + assert.deepEqual( + completion.items + .filter((item) => item.kind === CompletionItemKind.Constructor) + .map((item) => item.label), + ['None', 'Some'], + ) + + const definition = Document.definition(document, snapshot, someReference, () => undefined) + assert.deepEqual(definition?.targetSelectionRange.start, positionOf(source, 'Some', 0)) + assert.deepEqual( + Document.references(document, snapshot, someReference, true, () => undefined)?.map( + ({ range }) => range.start, + ), + [positionOf(source, 'Some', 0), someReference, positionOf(source, 'Some', 2)], + ) + + const help = Document.signatureHelp( + document, + snapshot, + positionAt(source, source.indexOf('value: 42') + 'value:'.length), + ) + assert.strictEqual(help?.signatures.at(0)?.label, 'Option.Some { value: T }: Option') + assert.deepEqual( + help?.signatures.at(0)?.parameters?.map((parameter) => parameter.label), + ['value: T'], + ) + + const symbols = Document.symbols(document, snapshot) + assert.deepEqual( + symbols.at(0)?.children?.map((symbol) => [symbol.name, symbol.kind]), + [ + ['Some', SymbolKind.EnumMember], + ['None', SymbolKind.EnumMember], + ], + ) + assert.deepEqual( + symbols + .at(0) + ?.children?.at(0) + ?.children?.map((symbol) => [symbol.name, symbol.kind]), + [['value', SymbolKind.Field]], + ) + }), +) + it.effect('lists constants, roles, functions, and structs with fields as document symbols', () => Effect.gen(function* () { const source = `pub const defaultAnswer: i32 = 42 From 665139b965316658839a4b7b5e5c4874bbc2a992 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 18:15:53 -0300 Subject: [PATCH 20/42] docs(stdlib): refresh nominal result examples --- apps/docs/content/language/stdlib/string.md | 16 ++++++++-------- packages/compiler/src/Stdlib.generated.ts | 4 ++-- .../compiler/src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/stdlib/silk/string.silk | 16 ++++++++-------- packages/docgen/test/Site.test.ts | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/docs/content/language/stdlib/string.md b/apps/docs/content/language/stdlib/string.md index 35b8746d1..e97f4f246 100644 --- a/apps/docs/content/language/stdlib/string.md +++ b/apps/docs/content/language/stdlib/string.md @@ -25,23 +25,23 @@ began. Start with [`scalarCursor`](#declaration-73696c6b2f737472696e673a3a736361 ### Validate borrowed UTF-8 bytes ```silk -import silk.result as Result +import silk.result { Result, unwrapOr } import silk.string as String +import silk.string { InvalidUtf8 } import silk.usize as usize pub fn main() -> i32 { let valid = String.fromUtf8(b"Silk") - |> Result.unwrapOr("") - let invalid = String.fromUtf8(b"a\x80") - match move invalid { - Result.Result.Success { .. } => return 0 - Result.Result.Failure { .. } => () - } + |> unwrapOr("") + let rejected = String.fromUtf8(b"a\x80") + |> unwrapOr("") let length = String.byteLength(valid) |> usize.toI32 - return length + 38 + let rejectedLength = String.byteLength(rejected) + |> usize.toI32 + return length + rejectedLength + 38 } ``` diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index 2a412e807..4c758cd23 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -1055,14 +1055,14 @@ export const modules = [ module: 'silk/string', path: 'silk/string.silk', sourceIdentity: 'silk/string', - digest: 'cdb2de4dd1b6053b0c5b3600bea695b1c84d61e3ccedac28d3d26de67b867dc6', + digest: '61a919d853b3617390ba90b51adff5895d18ab91a54aa904bdafe3e31dad3b86', documentation: 'silk/string.silk', layer: 'portable', runtimeInventory: ['stringByteLength', 'stringFromUtf8Unchecked', 'stringUtf8Bytes'], namespace: 'String', aliases: ['InvalidUtf8', 'ScalarCursor', 'ScalarStep'], source: - '//! Valid UTF-8 text, including owned storage, byte validation, and scalar-by-scalar traversal.\n//!\n//! # When to use\n//! Use the built-in `string` type for borrowed text and [`String`] when text must own its storage.\n//! Use [`Bytes`] when arbitrary octets must survive without UTF-8 validation.\n//!\n//! # Details\n//! [`fromUtf8`] validates and borrows existing bytes without allocating; [`copyUtf8`] validates and\n//! owns a copy. [`append`] and [`appendOwned`] leave the original value unchanged if growth cannot\n//! allocate. Scalar cursors expose Unicode scalar values and byte offsets, not grapheme clusters.\n//!\n//! # Gotchas\n//! A [`ScalarCursor`] is meaningful only for the same unchanged string from which its traversal\n//! began. Start with [`scalarCursor`] and advance only with [`nextCursor`].\n//!\n//! # Examples\n//! ## Validate borrowed UTF-8 bytes\n//! ```silk\n//! import silk.result as Result\n//!\n//! import silk.string as String\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let valid = String.fromUtf8(b"Silk")\n//! |> Result.unwrapOr("")\n//! let invalid = String.fromUtf8(b"a\\x80")\n//! match move invalid {\n//! Result.Result.Success { .. } => return 0\n//! Result.Result.Failure { .. } => ()\n//! }\n//! let length = String.byteLength(valid)\n//! |> usize.toI32\n//! return length + 38\n//! }\n//! ```\n//!\n//! ## Build owned text and read its first scalar\n//! ```silk\n//! import silk.char as char\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option as Option\n//!\n//! import silk.string as String\n//!\n//! import silk.u32 as u32\n//!\n//! fn scalarCode(step: String.ScalarStep) -> i32 {\n//! return String.scalarValue(&step)\n//! |> char.toU32\n//! |> u32.toI32\n//! }\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let copying = String.copy("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let mut text = run copying\n//! let appending = String.append(&mut text, "!")\n//! |> Effect.provideMut(&mut allocator)\n//! let appended = run appending\n//! let stepped = String.nextScalar(String.view(&text), String.scalarCursor())\n//! let mapped = Option.map(move stepped, scalarCode)\n//! let scalar = Option.unwrapOr(move mapped, 0)\n//! return scalar - 191\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n copy as bytesCopy,\n append as bytesAppend,\n asSlice as bytesAsSlice,\n length as bytesLength\n}\nimport silk.char as char\nimport silk.char { fromU32 as charFromU32 }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { Option, none, some }\nimport silk.result { Result, failResult, succeed }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// An owned sequence of valid UTF-8 bytes that releases its storage on drop.\npub struct String {\n bytes: Bytes\n}\n\n/// The first byte offset at which UTF-8 validation failed.\npub struct InvalidUtf8 {\n /// The zero-based offset of the first byte that cannot continue a valid UTF-8 sequence.\n pub offset: usize\n}\n\n/// An opaque UTF-8 position used for scalar-by-scalar traversal.\npub struct ScalarCursor {\n byteOffset: usize\n}\n\n/// One decoded Unicode scalar, its byte offset, and the cursor after it.\npub struct ScalarStep {\n scalar: char\n byteOffset: usize\n next: ScalarCursor\n}\n\nfn byte(value: u8) -> u8 {\n return value\n}\n\nfn continuation(value: u8) -> bool {\n if value < byte(128) { return false }\n return value <= byte(191)\n}\n\n/// Returns the first byte offset at which UTF-8 validation fails, or None for complete valid text.\nfn firstInvalidUtf8(values: &[u8]) -> Option {\n let mut index = usize.ZERO\n while index < values.length {\n let first = values[index]\n if first < byte(128) {\n index = index + usize.ONE\n } else {\n if first < byte(194) { return some(index) }\n if first <= byte(223) {\n if values.length <= index + usize.ONE { return some(index) }\n if continuation(values[index + usize.ONE]) == false {\n return some(index + usize.ONE)\n }\n index = index + 2\n } else {\n if first <= byte(239) {\n if values.length <= index + 2 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if first == byte(224) {\n if second < byte(160) { return some(index + usize.ONE) }\n }\n if first == byte(237) {\n if byte(159) < second { return some(index + usize.ONE) }\n }\n index = index + 3\n } else {\n if byte(244) < first { return some(index) }\n if values.length <= index + 3 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n let fourth = values[index + 3]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if continuation(fourth) == false { return some(index + 3) }\n if first == byte(240) {\n if second < byte(144) { return some(index + usize.ONE) }\n }\n if first == byte(244) {\n if byte(143) < second { return some(index + usize.ONE) }\n }\n index = index + 4\n }\n }\n }\n }\n return none()\n}\n\n/// Borrows caller-validated UTF-8 bytes as text without runtime validation.\n///\n/// # When to use\n///\n/// Use this function only when an earlier operation proves that the complete byte view is UTF-8.\n/// Use [`fromUtf8`] when the bytes have not been validated.\n///\n/// # Gotchas\n///\n/// The caller must guarantee that the complete byte view is valid UTF-8 for the lifetime of the\n/// returned string view. Invalid bytes violate the safety contract.\npub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(values) }\n return ""\n}\n\n/// Validates a complete byte view and borrows it as text without allocating.\n///\n/// # Details\n///\n/// Success returns a `string` view with the same lexical lifetime as `values`. Failure returns the\n/// first invalid byte offset in [`InvalidUtf8`].\npub fn fromUtf8(values: &[u8]) -> Result {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let text = unsafe fromUtf8Unchecked(values)\n return succeed(text)\n return failResult(InvalidUtf8 { offset: usize.ZERO })\n}\n\n/// Constructs an empty owned String without allocating.\npub fn make() -> String {\n return String { bytes: bytesMake() }\n}\n\n/// Copies valid borrowed text into independently owned storage.\npub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = Intrinsic.stringUtf8Bytes(value)\n let bytes = run bytesCopy(source)\n return String { bytes: move bytes }\n}\n\n/// Validates complete UTF-8 bytes and copies them into independently owned storage.\n///\n/// # When to use\n///\n/// Use this function when the bytes must outlive their current buffer. Use [`fromUtf8`] for a\n/// borrowed result without allocation.\n///\n/// # Details\n///\n/// Invalid input returns [`InvalidUtf8`] as ordinary result data. Allocation failure remains in the\n/// Effect failure channel. No owned string is returned in either failure case.\npub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let bytes = run bytesCopy(values)\n return succeed(String { bytes: move bytes })\n}\n\n// Appending grows the existing storage rather than copying the whole string into fresh storage\n// first. The atomicity is the same either way — the underlying byte append builds its replacement\n// buffer in full before committing, so a failed allocation leaves the original untouched — but the\n// cost is not: composing a message from several pieces is what this API is for, and a copy per\n// piece made that quadratic in the message and linear in allocations.\n/// Appends complete valid text atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function for borrowed text. Use [`appendOwned`] when the suffix is an owned [`String`].\n///\n/// # Details\n///\n/// If growth fails, `self` keeps its prior contents and byte length.\npub effect fn append(self: &mut String, value: string) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = Intrinsic.stringUtf8Bytes(value)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Appends another owned String atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function to consume an owned suffix. Use [`append`] when the suffix is borrowed text.\n///\n/// # Details\n///\n/// This function consumes `value`. If growth fails, `self` keeps its prior contents and byte length.\npub effect fn appendOwned(self: &mut String, value: String) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = bytesAsSlice(&value.bytes)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Borrows the complete owned contents as valid text without allocating or copying.\npub fn view(self: &String) -> string {\n let bytes = bytesAsSlice(&self.bytes)\n return unsafe fromUtf8Unchecked(bytes)\n}\n\n/// Borrows a string\'s immutable UTF-8 encoding.\npub fn utf8Bytes(value: string) -> &[u8] {\n return Intrinsic.stringUtf8Bytes(value)\n}\n\n/// Returns a string\'s UTF-8 byte length.\npub fn byteLength(value: string) -> usize {\n return Intrinsic.stringByteLength(value)\n}\n\n/// Borrows an owned String\'s immutable UTF-8 encoding.\npub fn ownedUtf8Bytes(self: &String) -> &[u8] {\n return bytesAsSlice(&self.bytes)\n}\n\n/// Returns an owned String\'s initialized UTF-8 byte length.\npub fn ownedByteLength(self: &String) -> usize {\n return bytesLength(&self.bytes)\n}\n\n/// Creates a cursor at UTF-8 byte offset zero, before the first Unicode scalar.\npub fn scalarCursor() -> ScalarCursor {\n return ScalarCursor { byteOffset: usize.ZERO }\n}\n\n/// Returns a cursor\'s explicit UTF-8 byte offset.\npub fn cursorByteOffset(cursor: &ScalarCursor) -> usize {\n return cursor.byteOffset\n}\n\n/// Returns the decoded Unicode scalar value without consuming the step.\npub fn scalarValue(step: &ScalarStep) -> char {\n return step.scalar\n}\n\n/// Returns the UTF-8 byte offset at which one step begins.\npub fn scalarByteOffset(step: &ScalarStep) -> usize {\n return step.byteOffset\n}\n\n/// Consumes one scalar step and returns the cursor immediately after that scalar.\npub fn nextCursor(step: ScalarStep) -> ScalarCursor {\n return match move step {\n ScalarStep { scalar, byteOffset, next } => move next\n }\n}\n\nfn scalar32(value: u8) -> u32 {\n return u8.toU32(value)\n}\n\n/// Decodes the scalar at a cursor, or returns `None` at the end of the string.\n///\n/// # Details\n///\n/// A present step contains the scalar, its starting byte offset, and the next cursor. This function\n/// does not allocate.\n///\n/// # Gotchas\n///\n/// The cursor must come from [`scalarCursor`] or [`nextCursor`] for the same unchanged string.\npub fn nextScalar(value: string, cursor: ScalarCursor) -> Option {\n let bytes = Intrinsic.stringUtf8Bytes(value)\n let offset = cursor.byteOffset\n if offset == bytes.length { return none() }\n let first = bytes[offset]\n let mut scalar = scalar32(first)\n let mut width = usize.ONE\n if byte(194) <= first {\n if first <= byte(223) {\n scalar = (scalar32(first) - u32.toU32(192)) * u32.toU32(64)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128))\n width = 2\n } else {\n if first <= byte(239) {\n scalar = (scalar32(first) - u32.toU32(224)) * u32.toU32(4096)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128))\n width = 3\n } else {\n scalar = (scalar32(first) - u32.toU32(240)) * u32.toU32(262144)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(4096)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 3]) - u32.toU32(128))\n width = 4\n }\n }\n }\n return match move charFromU32(scalar) {\n Option.Some { value: decoded } => some(ScalarStep {\n scalar: decoded,\n byteOffset: offset,\n next: ScalarCursor { byteOffset: offset + width }\n })\n Option.None => none()\n }\n}\n', + '//! Valid UTF-8 text, including owned storage, byte validation, and scalar-by-scalar traversal.\n//!\n//! # When to use\n//! Use the built-in `string` type for borrowed text and [`String`] when text must own its storage.\n//! Use [`Bytes`] when arbitrary octets must survive without UTF-8 validation.\n//!\n//! # Details\n//! [`fromUtf8`] validates and borrows existing bytes without allocating; [`copyUtf8`] validates and\n//! owns a copy. [`append`] and [`appendOwned`] leave the original value unchanged if growth cannot\n//! allocate. Scalar cursors expose Unicode scalar values and byte offsets, not grapheme clusters.\n//!\n//! # Gotchas\n//! A [`ScalarCursor`] is meaningful only for the same unchanged string from which its traversal\n//! began. Start with [`scalarCursor`] and advance only with [`nextCursor`].\n//!\n//! # Examples\n//! ## Validate borrowed UTF-8 bytes\n//! ```silk\n//! import silk.result { Result, unwrapOr }\n//!\n//! import silk.string as String\n//! import silk.string { InvalidUtf8 }\n//!\n//! import silk.usize as usize\n//!\n//! pub fn main() -> i32 {\n//! let valid = String.fromUtf8(b"Silk")\n//! |> unwrapOr("")\n//! let rejected = String.fromUtf8(b"a\\x80")\n//! |> unwrapOr("")\n//! let length = String.byteLength(valid)\n//! |> usize.toI32\n//! let rejectedLength = String.byteLength(rejected)\n//! |> usize.toI32\n//! return length + rejectedLength + 38\n//! }\n//! ```\n//!\n//! ## Build owned text and read its first scalar\n//! ```silk\n//! import silk.char as char\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option as Option\n//!\n//! import silk.string as String\n//!\n//! import silk.u32 as u32\n//!\n//! fn scalarCode(step: String.ScalarStep) -> i32 {\n//! return String.scalarValue(&step)\n//! |> char.toU32\n//! |> u32.toI32\n//! }\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let copying = String.copy("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let mut text = run copying\n//! let appending = String.append(&mut text, "!")\n//! |> Effect.provideMut(&mut allocator)\n//! let appended = run appending\n//! let stepped = String.nextScalar(String.view(&text), String.scalarCursor())\n//! let mapped = Option.map(move stepped, scalarCode)\n//! let scalar = Option.unwrapOr(move mapped, 0)\n//! return scalar - 191\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n copy as bytesCopy,\n append as bytesAppend,\n asSlice as bytesAsSlice,\n length as bytesLength\n}\nimport silk.char as char\nimport silk.char { fromU32 as charFromU32 }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { Option, none, some }\nimport silk.result { Result, failResult, succeed }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// An owned sequence of valid UTF-8 bytes that releases its storage on drop.\npub struct String {\n bytes: Bytes\n}\n\n/// The first byte offset at which UTF-8 validation failed.\npub struct InvalidUtf8 {\n /// The zero-based offset of the first byte that cannot continue a valid UTF-8 sequence.\n pub offset: usize\n}\n\n/// An opaque UTF-8 position used for scalar-by-scalar traversal.\npub struct ScalarCursor {\n byteOffset: usize\n}\n\n/// One decoded Unicode scalar, its byte offset, and the cursor after it.\npub struct ScalarStep {\n scalar: char\n byteOffset: usize\n next: ScalarCursor\n}\n\nfn byte(value: u8) -> u8 {\n return value\n}\n\nfn continuation(value: u8) -> bool {\n if value < byte(128) { return false }\n return value <= byte(191)\n}\n\n/// Returns the first byte offset at which UTF-8 validation fails, or None for complete valid text.\nfn firstInvalidUtf8(values: &[u8]) -> Option {\n let mut index = usize.ZERO\n while index < values.length {\n let first = values[index]\n if first < byte(128) {\n index = index + usize.ONE\n } else {\n if first < byte(194) { return some(index) }\n if first <= byte(223) {\n if values.length <= index + usize.ONE { return some(index) }\n if continuation(values[index + usize.ONE]) == false {\n return some(index + usize.ONE)\n }\n index = index + 2\n } else {\n if first <= byte(239) {\n if values.length <= index + 2 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if first == byte(224) {\n if second < byte(160) { return some(index + usize.ONE) }\n }\n if first == byte(237) {\n if byte(159) < second { return some(index + usize.ONE) }\n }\n index = index + 3\n } else {\n if byte(244) < first { return some(index) }\n if values.length <= index + 3 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n let fourth = values[index + 3]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if continuation(fourth) == false { return some(index + 3) }\n if first == byte(240) {\n if second < byte(144) { return some(index + usize.ONE) }\n }\n if first == byte(244) {\n if byte(143) < second { return some(index + usize.ONE) }\n }\n index = index + 4\n }\n }\n }\n }\n return none()\n}\n\n/// Borrows caller-validated UTF-8 bytes as text without runtime validation.\n///\n/// # When to use\n///\n/// Use this function only when an earlier operation proves that the complete byte view is UTF-8.\n/// Use [`fromUtf8`] when the bytes have not been validated.\n///\n/// # Gotchas\n///\n/// The caller must guarantee that the complete byte view is valid UTF-8 for the lifetime of the\n/// returned string view. Invalid bytes violate the safety contract.\npub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(values) }\n return ""\n}\n\n/// Validates a complete byte view and borrows it as text without allocating.\n///\n/// # Details\n///\n/// Success returns a `string` view with the same lexical lifetime as `values`. Failure returns the\n/// first invalid byte offset in [`InvalidUtf8`].\npub fn fromUtf8(values: &[u8]) -> Result {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let text = unsafe fromUtf8Unchecked(values)\n return succeed(text)\n return failResult(InvalidUtf8 { offset: usize.ZERO })\n}\n\n/// Constructs an empty owned String without allocating.\npub fn make() -> String {\n return String { bytes: bytesMake() }\n}\n\n/// Copies valid borrowed text into independently owned storage.\npub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = Intrinsic.stringUtf8Bytes(value)\n let bytes = run bytesCopy(source)\n return String { bytes: move bytes }\n}\n\n/// Validates complete UTF-8 bytes and copies them into independently owned storage.\n///\n/// # When to use\n///\n/// Use this function when the bytes must outlive their current buffer. Use [`fromUtf8`] for a\n/// borrowed result without allocation.\n///\n/// # Details\n///\n/// Invalid input returns [`InvalidUtf8`] as ordinary result data. Allocation failure remains in the\n/// Effect failure channel. No owned string is returned in either failure case.\npub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let bytes = run bytesCopy(values)\n return succeed(String { bytes: move bytes })\n}\n\n// Appending grows the existing storage rather than copying the whole string into fresh storage\n// first. The atomicity is the same either way — the underlying byte append builds its replacement\n// buffer in full before committing, so a failed allocation leaves the original untouched — but the\n// cost is not: composing a message from several pieces is what this API is for, and a copy per\n// piece made that quadratic in the message and linear in allocations.\n/// Appends complete valid text atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function for borrowed text. Use [`appendOwned`] when the suffix is an owned [`String`].\n///\n/// # Details\n///\n/// If growth fails, `self` keeps its prior contents and byte length.\npub effect fn append(self: &mut String, value: string) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = Intrinsic.stringUtf8Bytes(value)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Appends another owned String atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function to consume an owned suffix. Use [`append`] when the suffix is borrowed text.\n///\n/// # Details\n///\n/// This function consumes `value`. If growth fails, `self` keeps its prior contents and byte length.\npub effect fn appendOwned(self: &mut String, value: String) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = bytesAsSlice(&value.bytes)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Borrows the complete owned contents as valid text without allocating or copying.\npub fn view(self: &String) -> string {\n let bytes = bytesAsSlice(&self.bytes)\n return unsafe fromUtf8Unchecked(bytes)\n}\n\n/// Borrows a string\'s immutable UTF-8 encoding.\npub fn utf8Bytes(value: string) -> &[u8] {\n return Intrinsic.stringUtf8Bytes(value)\n}\n\n/// Returns a string\'s UTF-8 byte length.\npub fn byteLength(value: string) -> usize {\n return Intrinsic.stringByteLength(value)\n}\n\n/// Borrows an owned String\'s immutable UTF-8 encoding.\npub fn ownedUtf8Bytes(self: &String) -> &[u8] {\n return bytesAsSlice(&self.bytes)\n}\n\n/// Returns an owned String\'s initialized UTF-8 byte length.\npub fn ownedByteLength(self: &String) -> usize {\n return bytesLength(&self.bytes)\n}\n\n/// Creates a cursor at UTF-8 byte offset zero, before the first Unicode scalar.\npub fn scalarCursor() -> ScalarCursor {\n return ScalarCursor { byteOffset: usize.ZERO }\n}\n\n/// Returns a cursor\'s explicit UTF-8 byte offset.\npub fn cursorByteOffset(cursor: &ScalarCursor) -> usize {\n return cursor.byteOffset\n}\n\n/// Returns the decoded Unicode scalar value without consuming the step.\npub fn scalarValue(step: &ScalarStep) -> char {\n return step.scalar\n}\n\n/// Returns the UTF-8 byte offset at which one step begins.\npub fn scalarByteOffset(step: &ScalarStep) -> usize {\n return step.byteOffset\n}\n\n/// Consumes one scalar step and returns the cursor immediately after that scalar.\npub fn nextCursor(step: ScalarStep) -> ScalarCursor {\n return match move step {\n ScalarStep { scalar, byteOffset, next } => move next\n }\n}\n\nfn scalar32(value: u8) -> u32 {\n return u8.toU32(value)\n}\n\n/// Decodes the scalar at a cursor, or returns `None` at the end of the string.\n///\n/// # Details\n///\n/// A present step contains the scalar, its starting byte offset, and the next cursor. This function\n/// does not allocate.\n///\n/// # Gotchas\n///\n/// The cursor must come from [`scalarCursor`] or [`nextCursor`] for the same unchanged string.\npub fn nextScalar(value: string, cursor: ScalarCursor) -> Option {\n let bytes = Intrinsic.stringUtf8Bytes(value)\n let offset = cursor.byteOffset\n if offset == bytes.length { return none() }\n let first = bytes[offset]\n let mut scalar = scalar32(first)\n let mut width = usize.ONE\n if byte(194) <= first {\n if first <= byte(223) {\n scalar = (scalar32(first) - u32.toU32(192)) * u32.toU32(64)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128))\n width = 2\n } else {\n if first <= byte(239) {\n scalar = (scalar32(first) - u32.toU32(224)) * u32.toU32(4096)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128))\n width = 3\n } else {\n scalar = (scalar32(first) - u32.toU32(240)) * u32.toU32(262144)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(4096)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 3]) - u32.toU32(128))\n width = 4\n }\n }\n }\n return match move charFromU32(scalar) {\n Option.Some { value: decoded } => some(ScalarStep {\n scalar: decoded,\n byteOffset: offset,\n next: ScalarCursor { byteOffset: offset + width }\n })\n Option.None => none()\n }\n}\n', }, { module: 'silk/system_clock', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 89fd40c5a..51fa8178c 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '9919088e2dcdacd025e3cb7a605714c7608bb226f5ddc9345c2ec1efe9962bca' +export const compilerDigest = '72dbac967a0902952bbe4c39a97e5c2e9bdd57a588b479a7bf3308583216d61a' diff --git a/packages/compiler/stdlib/silk/string.silk b/packages/compiler/stdlib/silk/string.silk index 4d54bce8a..f293332d6 100644 --- a/packages/compiler/stdlib/silk/string.silk +++ b/packages/compiler/stdlib/silk/string.silk @@ -16,23 +16,23 @@ //! # Examples //! ## Validate borrowed UTF-8 bytes //! ```silk -//! import silk.result as Result +//! import silk.result { Result, unwrapOr } //! //! import silk.string as String +//! import silk.string { InvalidUtf8 } //! //! import silk.usize as usize //! //! pub fn main() -> i32 { //! let valid = String.fromUtf8(b"Silk") -//! |> Result.unwrapOr("") -//! let invalid = String.fromUtf8(b"a\x80") -//! match move invalid { -//! Result.Result.Success { .. } => return 0 -//! Result.Result.Failure { .. } => () -//! } +//! |> unwrapOr("") +//! let rejected = String.fromUtf8(b"a\x80") +//! |> unwrapOr("") //! let length = String.byteLength(valid) //! |> usize.toI32 -//! return length + 38 +//! let rejectedLength = String.byteLength(rejected) +//! |> usize.toI32 +//! return length + rejectedLength + 38 //! } //! ``` //! diff --git a/packages/docgen/test/Site.test.ts b/packages/docgen/test/Site.test.ts index 62bb564ef..75d9e0489 100644 --- a/packages/docgen/test/Site.test.ts +++ b/packages/docgen/test/Site.test.ts @@ -84,7 +84,7 @@ it.effect( assert.include(option.contents, 'unwrapOr') assert.include( option.contents, - 'pub fn unwrapOr<T>(self: Option<T>, fallback: T) -> T', + 'pub fn unwrapOr<T>(self: silk/option.Option<T>, fallback: T) -> T', 'a signature must be escaped, not emitted as markup', ) assert.notInclude( From 3d6d7b57bdba2587601de69fdd6f640255c2946a Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 18:22:54 -0300 Subject: [PATCH 21/42] feat(docgen): link nominal union variants --- apps/docs/content/language/stdlib/effect.md | 2 +- .../content/language/stdlib/filesystem.md | 2 +- .../content/language/stdlib/host-input.md | 2 +- apps/docs/content/language/stdlib/option.md | 6 +- .../content/language/stdlib/os-host-input.md | 2 +- apps/docs/content/language/stdlib/result.md | 6 +- packages/docgen/src/Document.ts | 2 + packages/docgen/src/Model.ts | 4 ++ packages/docgen/src/Project.ts | 72 ++++++++++++++++--- packages/docgen/test/Model.test.ts | 35 ++++++++- packages/docgen/test/Project.test.ts | 10 ++- 11 files changed, 123 insertions(+), 20 deletions(-) diff --git a/apps/docs/content/language/stdlib/effect.md b/apps/docs/content/language/stdlib/effect.md index 151cb2134..8b67bb2c7 100644 --- a/apps/docs/content/language/stdlib/effect.md +++ b/apps/docs/content/language/stdlib/effect.md @@ -241,7 +241,7 @@ Executes `protected` once and converts its success or typed failure into ordinar ### Details The returned Effect still requires `R`, because conversion does not provide services. Its typed -failure row is empty: an `E` becomes `Failure` data instead of propagating. Traps are not typed +failure row is empty: an `E` becomes [`Failure`](./result.md#declaration-73696c6b2f726573756c743a3a526573756c743a3a76617269616e743a31) data instead of propagating. Traps are not typed failures and therefore are not captured. ### Examples diff --git a/apps/docs/content/language/stdlib/filesystem.md b/apps/docs/content/language/stdlib/filesystem.md index 8f0d8ee80..5e805cc13 100644 --- a/apps/docs/content/language/stdlib/filesystem.md +++ b/apps/docs/content/language/stdlib/filesystem.md @@ -691,7 +691,7 @@ fails with the `InvalidPath` reason. Resolution is lexical and never accesses th pub effect fn parent(self: &silk/filesystem.Path) -> silk/option.Option ! OutOfMemoryError ? &mut Allocator ``` -Allocates an independently owned parent path, or `None` when `self` is root. +Allocates an independently owned parent path, or [`None`](./option.md#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a30) when `self` is root. ### Details diff --git a/apps/docs/content/language/stdlib/host-input.md b/apps/docs/content/language/stdlib/host-input.md index 707808183..5d7e766dd 100644 --- a/apps/docs/content/language/stdlib/host-input.md +++ b/apps/docs/content/language/stdlib/host-input.md @@ -13,7 +13,7 @@ lossless pass-through. ## Details Arguments include the program name at index zero and retain host order. A missing argument index -or unset variable is `None`, while [`HostInputError`](#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e7075744572726f72) means the provider could not answer. +or unset variable is [`None`](./option.md#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a30), while [`HostInputError`](#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e7075744572726f72) means the provider could not answer. Returned [`Bytes`](./bytes.md#declaration-73696c6b2f62797465733a3a4279746573) values are independently owned, so lookup operations also carry explicit [`OutOfMemoryError`](./allocator.md#declaration-73696c6b2f616c6c6f6361746f723a3a4f75744f664d656d6f72794572726f72) and [`Allocator`](./allocator.md#declaration-73696c6b2f616c6c6f6361746f723a3a416c6c6f6361746f72) channels. diff --git a/apps/docs/content/language/stdlib/option.md b/apps/docs/content/language/stdlib/option.md index c0b9427aa..5df03944e 100644 --- a/apps/docs/content/language/stdlib/option.md +++ b/apps/docs/content/language/stdlib/option.md @@ -57,7 +57,7 @@ Public declarations: 6. pub union Option ``` -An owned value that is either `Some` or `None`. +An owned value that is either [`Some`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a31) or [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a30). ### Details @@ -116,7 +116,7 @@ Applies `transform` once to a present value and keeps an absent value absent. ### Details -The callback is not called for `None`. This operation consumes `self`; use a shared borrow and +The callback is not called for [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a30). This operation consumes `self`; use a shared borrow and `match` instead when the original option must remain available. @@ -132,7 +132,7 @@ outcome stays one Option deep instead of nesting. ### Details -The callback runs once for `Some` and not at all for `None`. Use this when the next step may +The callback runs once for [`Some`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a31) and not at all for [`None`](#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a30). Use this when the next step may reject the value without needing to explain why; use a `Result` when rejection needs an error. diff --git a/apps/docs/content/language/stdlib/os-host-input.md b/apps/docs/content/language/stdlib/os-host-input.md index 2a8f5196e..54623ee52 100644 --- a/apps/docs/content/language/stdlib/os-host-input.md +++ b/apps/docs/content/language/stdlib/os-host-input.md @@ -14,7 +14,7 @@ the program under test. The provider owns no persistent state. Each successful lookup copies the host value into independent [`Bytes`](./bytes.md#declaration-73696c6b2f62797465733a3a4279746573), beginning with a bounded buffer and retrying once at the exact size the -boundary reports. An absent argument or variable remains `None`; an unavailable working +boundary reports. An absent argument or variable remains [`None`](./option.md#declaration-73696c6b2f6f7074696f6e3a3a4f7074696f6e3a3a76617269616e743a30); an unavailable working directory and contradictory host lengths become [`HostInputError`](./host-input.md#declaration-73696c6b2f686f73745f696e7075743a3a486f7374496e7075744572726f72). Constructing the provider reads no host state. Portable code performs lookups after the diff --git a/apps/docs/content/language/stdlib/result.md b/apps/docs/content/language/stdlib/result.md index 81a6ecc2c..9f0dd3030 100644 --- a/apps/docs/content/language/stdlib/result.md +++ b/apps/docs/content/language/stdlib/result.md @@ -12,7 +12,7 @@ success continuation that already returns a result. ## Details -`Result` owns either `Success` or `Failure`. Its combinators move the selected payload +`Result` owns either [`Success`](#declaration-73696c6b2f726573756c743a3a526573756c743a3a76617269616e743a30) or [`Failure`](#declaration-73696c6b2f726573756c743a3a526573756c743a3a76617269616e743a31). Its combinators move the selected payload forward and preserve the other arm without inventing a runtime failure-row descriptor. Unlike an `Effect`, a `Result` is already completed ordinary data: it does not run, @@ -119,7 +119,7 @@ Applies `transform` once to a success value and carries a failure through unchan ### Details -The callback is never called for `Failure`. This consumes the result and may change only its +The callback is never called for [`Failure`](#declaration-73696c6b2f726573756c743a3a526573756c743a3a76617269616e743a31). This consumes the result and may change only its success type; use [`mapError`](#declaration-73696c6b2f726573756c743a3a6d61704572726f72) to change the failure type instead. @@ -134,7 +134,7 @@ Applies `transform` once to a failure value and carries a success through unchan ### Details -The callback is never called for `Success`. This consumes the result and may change only its +The callback is never called for [`Success`](#declaration-73696c6b2f726573756c743a3a526573756c743a3a76617269616e743a30). This consumes the result and may change only its failure type. diff --git a/packages/docgen/src/Document.ts b/packages/docgen/src/Document.ts index e3a034196..8fcdd3293 100644 --- a/packages/docgen/src/Document.ts +++ b/packages/docgen/src/Document.ts @@ -25,7 +25,9 @@ export interface LinkTarget { | 'Function' | 'Struct' | 'Enum' + | 'EnumMember' | 'Union' + | 'UnionVariant' | 'Service' | 'Interface' | 'Constant' diff --git a/packages/docgen/src/Model.ts b/packages/docgen/src/Model.ts index beae01b65..a81dbcecd 100644 --- a/packages/docgen/src/Model.ts +++ b/packages/docgen/src/Model.ts @@ -20,7 +20,9 @@ export interface LinkTarget { | 'Function' | 'Struct' | 'Enum' + | 'EnumMember' | 'Union' + | 'UnionVariant' | 'Service' | 'Interface' | 'Constant' @@ -201,7 +203,9 @@ const linkKindOf = (value: unknown): LinkTarget['kind'] | undefined => { case 'Function': case 'Struct': case 'Enum': + case 'EnumMember': case 'Union': + case 'UnionVariant': case 'Service': case 'Interface': case 'Constant': diff --git a/packages/docgen/src/Project.ts b/packages/docgen/src/Project.ts index 35ecbd960..aa1510dc8 100644 --- a/packages/docgen/src/Project.ts +++ b/packages/docgen/src/Project.ts @@ -129,17 +129,73 @@ const targetOf = ( spelling: string, ): Document.LinkTarget | undefined => { const lookup = Analysis.lookupName(snapshot, module, spelling) - if (lookup._tag !== 'Resolved') return undefined + if (lookup._tag === 'Resolved') { + const targetModule = + lookup.declaration.canonical._tag === 'Canonical' + ? lookup.declaration.canonical.id.module + : lookup.declaration.id.sourceId + const targetName = nameOf(lookup.declaration.name, spelling) + return Object.freeze({ + id: declarationId(targetModule, lookup.declaration), + module: targetModule, + name: targetName, + kind: linkTargetKind(lookup.declaration), + }) + } + + const index = Analysis.declarationIndex(snapshot) + const parentIsVisible = (parent: DeclarationFacts.MemberFact): boolean => { + if (parent.name._tag !== 'Present') return false + const visible = Analysis.lookupName(snapshot, module, parent.name.spelling) + return visible._tag === 'Resolved' && visible.declaration === parent + } + const enumMembers = index.modules.flatMap((headers) => + headers.enums.flatMap((enum_) => + parentIsVisible(enum_) + ? enum_.members.flatMap((member) => + member.name._tag === 'Present' && member.name.spelling === spelling + ? [Object.freeze({ enum_, member })] + : [], + ) + : [], + ), + ) + const unionVariants = index.modules.flatMap((headers) => + headers.unions.flatMap((union) => + parentIsVisible(union) + ? union.variants.flatMap((variant) => + variant.name._tag === 'Present' && variant.name.spelling === spelling + ? [Object.freeze({ union, variant })] + : [], + ) + : [], + ), + ) + if (enumMembers.length + unionVariants.length !== 1) return undefined + const enumMember = enumMembers.at(0) + if (enumMember !== undefined) { + const targetModule = + enumMember.enum_.canonical._tag === 'Canonical' + ? enumMember.enum_.canonical.id.module + : enumMember.enum_.id.sourceId + return Object.freeze({ + id: `${declarationId(targetModule, enumMember.enum_)}::member:${enumMember.member.id.ordinal}`, + module: targetModule, + name: spelling, + kind: 'EnumMember', + }) + } + const unionVariant = unionVariants.at(0) + if (unionVariant === undefined) return undefined const targetModule = - lookup.declaration.canonical._tag === 'Canonical' - ? lookup.declaration.canonical.id.module - : lookup.declaration.id.sourceId - const targetName = nameOf(lookup.declaration.name, spelling) + unionVariant.union.canonical._tag === 'Canonical' + ? unionVariant.union.canonical.id.module + : unionVariant.union.id.sourceId return Object.freeze({ - id: declarationId(targetModule, lookup.declaration), + id: `${declarationId(targetModule, unionVariant.union)}::variant:${unionVariant.variant.id.ordinal}`, module: targetModule, - name: targetName, - kind: linkTargetKind(lookup.declaration), + name: spelling, + kind: 'UnionVariant', }) } diff --git a/packages/docgen/test/Model.test.ts b/packages/docgen/test/Model.test.ts index fac3b606c..1cd28cf09 100644 --- a/packages/docgen/test/Model.test.ts +++ b/packages/docgen/test/Model.test.ts @@ -47,10 +47,43 @@ it('decodes the canonical signature object and required declaration fields', () ) }) -it('decodes enum, union, child, and role declaration kinds', () => { +it('decodes enum, union, child, role, and link target kinds', () => { for (const kind of ['Enum', 'EnumMember', 'Union', 'UnionVariant', 'Role']) { assert.strictEqual(Model.decode(project(item({ kind })))._tag, 'Decoded', kind) } + for (const kind of ['EnumMember', 'UnionVariant']) { + const document = { + _tag: 'Document', + source, + markdown: 'Variant.', + blocks: [ + { + _tag: 'Paragraph', + children: [ + { + _tag: 'SymbolLink', + spelling: 'Variant', + target: { + id: 'project/main::Variant', + module: 'project/main', + name: 'Variant', + kind, + }, + source, + }, + ], + source, + }, + ], + examples: [], + fallback: false, + } + assert.strictEqual( + Model.decode(project(item({ documentation: document })))._tag, + 'Decoded', + kind, + ) + } }) it('rejects historical signatures and missing declaration fields atomically', () => { diff --git a/packages/docgen/test/Project.test.ts b/packages/docgen/test/Project.test.ts index 5b3e5ca7e..e4aa79871 100644 --- a/packages/docgen/test/Project.test.ts +++ b/packages/docgen/test/Project.test.ts @@ -41,7 +41,7 @@ pub enum State { Ready } -/// A computation outcome. +/// A computation with a [\`Success\`] variant. pub union Outcome { /// A successful payload. Success { pub value: T }, @@ -91,6 +91,14 @@ pub union Outcome { ) assert.strictEqual(variants?.at(0)?.documentation?.markdown, 'A successful payload.') assert.strictEqual(variants?.at(0)?.children.at(0)?.name, 'value') + const outcomeLink = outcome?.documentation?.blocks + .flatMap((block) => (block._tag === 'Paragraph' ? block.children : [])) + .find((inline) => inline._tag === 'SymbolLink') + assert.strictEqual(outcomeLink?._tag, 'SymbolLink') + if (outcomeLink?._tag === 'SymbolLink') { + assert.strictEqual(outcomeLink.target?.kind, 'UnionVariant') + assert.strictEqual(outcomeLink.target?.id, 'project/main::Outcome::variant:0') + } const privateProject = Project.make(snapshot, { includePrivate: true }) assert.isTrue(privateProject.modules[0]?.items.some((item) => item.name === 'helper')) From 14e1785f0dd58863af330b998f55f49acfdb4cae Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 18:29:45 -0300 Subject: [PATCH 22/42] fix(compiler): validate relay and union ownership precisely --- apps/docs/content/language/stdlib/effect.md | 4 ---- packages/compiler/src/MirVerification.ts | 14 +++++--------- packages/compiler/src/Stdlib.generated.ts | 4 ++-- packages/compiler/stdlib/silk/effect.silk | 4 ---- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/apps/docs/content/language/stdlib/effect.md b/apps/docs/content/language/stdlib/effect.md index 8b67bb2c7..098c9e553 100644 --- a/apps/docs/content/language/stdlib/effect.md +++ b/apps/docs/content/language/stdlib/effect.md @@ -271,10 +271,6 @@ pub fn main() -> i32 { } ``` -This is ordinary Silk composition: success is mapped into `Result.Success`, then `catchAll` -maps the complete typed failure value into `Result.Failure`. Compound failure unions and -requirements are preserved, and the exact `once fn` adapters transfer affine payloads once. - ## `mapBoth` diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 89aa50c29..6347f7ae8 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -2450,13 +2450,7 @@ export const verify = (self: Module): ReadonlyArray => { region.runner.classification === 'Unknown' ) return [] - const declaration = region.runner.declaration - return declaration === undefined || - !self.functions.some( - (candidate) => - originReachable.has(instanceText(candidate.instance)) && - matchesInstance(candidate, declaration, region.runner.typeArguments), - ) + return !originReachable.has(instanceText(fn.instance)) ? [Object.freeze({ fn, region })] : [] }), @@ -2468,7 +2462,7 @@ export const verify = (self: Module): ReadonlyArray => { _tag: 'Violation', rule: 'OrphanSuspensionMachinery', function: orphanRelay.fn.id, - detail: `suspendable runner ${orphanRelay.region.runner.declaration === undefined ? 'unknown' : targetText(orphanRelay.region.runner.declaration)} has no reachable explicit transfer origin (origin-reachable: ${ + detail: `suspendable relay through ${orphanRelay.region.runner.declaration === undefined ? 'an unknown runner' : targetText(orphanRelay.region.runner.declaration)} belongs to a function with no reachable explicit transfer origin (origin-reachable: ${ self.functions .filter((fn) => originReachable.has(instanceText(fn.instance))) .map((fn) => targetText(fn.id)) @@ -4557,7 +4551,9 @@ export const verify = (self: Module): ReadonlyArray => { : coverageFieldPathType(self.layout, arm.member, entry.path) return ( selected !== undefined && - cleanupMatchesSemanticType(self.layout, entry.cleanup, selected) + (arm.member?._tag === 'NominalUnionVariant' + ? cleanupMatchesSemanticType(self.layout, entry.cleanup, selected) + : SilkType.equals(selected, entry.cleanup.type)) ) }) : arm.selected.cleanup.length === 0) diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index 4c758cd23..ea8845157 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -96,7 +96,7 @@ export const modules = [ module: 'silk/effect', path: 'silk/effect.silk', sourceIdentity: 'silk/effect', - digest: 'ff1454c143be6d6c1406a0ad284d8c9a1d26d56ef3228d63318881f8cead95b3', + digest: '4ec13a79a9f01a85a700cc95b2e0171c8c395aeff65a9f4d96d39b53f2a4dede', documentation: 'silk/effect.silk', layer: 'portable', runtimeInventory: [ @@ -108,7 +108,7 @@ export const modules = [ ], namespace: 'Effect', source: - "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core catches typed\n// failures and binds typed requirements; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because conversion does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\n///\n/// This is ordinary Silk composition: success is mapped into `Result.Success`, then `catchAll`\n/// maps the complete typed failure value into `Result.Failure`. Compound failure unions and\n/// requirements are preserved, and the exact `once fn` adapters transfer affine payloads once.\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is converted into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", + "//! Builds lazy computations by transforming success, recovering typed failure, supplying services,\n//! and controlling sequencing and cleanup.\n//!\n//! # When to use\n//! An `Effect` describes a computation with three visible channels: it can succeed with\n//! `A`, fail with typed value `E`, and require providers `R`. Use [`map`] and [`flatMap`] to continue\n//! success, [`mapError`], [`catch`], or [`catchAll`] for typed failures, [`provide`] or [`provideMut`]\n//! for lexical services, and [`ensuring`] for typed-outcome cleanup. Direct `run` remains clearest\n//! for straightforward sequential code.\n//!\n//! # Details\n//! Combinators are lazy: passing an Effect does not run it. Most accept a `once Effect`, so that\n//! particular value can execute at most once; [`retry`] explicitly accepts a reusable Effect.\n//! Sequential combinators stop at the first typed failure unless a recovery operation handles it.\n//! Their signatures show how failure and requirement rows combine, so composing two steps normally\n//! produces the unions `! E | F` and `? R | S`.\n//!\n//! A provider operation removes one exact capability, role, and access entry from the requirement\n//! row. When one provider could satisfy multiple entries, select the intended entry explicitly as\n//! the first generic argument, for example `provideMut`. Shared, exclusive, and\n//! owned provider bindings have distinct borrowing and capture behavior.\n//!\n//! # Gotchas\n//! Typed failures are outcomes that combinators can materialize and recover. Traps are not: they bypass\n//! [`catchAll`], [`ensuring`], and Drop hooks. [`suspend`] crosses the stack-safe execution boundary\n//! while preserving all three channels exactly; frame exhaustion is fatal.\n//!\n//! # Examples\n//! ## Transform and continue a successful computation\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! code: i32\n//! }\n//!\n//! effect fn read(value: i32) -> i32\n//! ! Problem {\n//! if value < 0 {\n//! fail Problem {code: 0}\n//! }\n//! return value\n//! }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! effect fn addTwo(value: i32) -> i32\n//! ! Problem {\n//! return value + 2\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.code\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let computation = read(20)\n//! |> Effect.map(double)\n//! |> Effect.flatMap(addTwo)\n//! return run Effect.catchAll(computation, recover)\n//! }\n//! ```\n//!\n//! ## Supply a custom service for one lexical computation\n//!\n//! Operation is declared inline below.\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! service Clock {\n//! effect fn value() -> i32 ? &Clock\n//! }\n//!\n//! struct FixedClock {\n//! value: i32\n//! }\n//!\n//! impl Clock for FixedClock {\n//! effect fn value(self: &Self) -> i32 {\n//! return self.value\n//! }\n//! }\n//!\n//! effect fn readClock() -> i32\n//! ? &Clock {\n//! return run Clock.value()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let clock = FixedClock {value: 42}\n//! return run Effect.provide(readClock(), &clock)\n//! }\n//! ```\n//!\n//! ## Recover a typed failure into success\n//! ```silk\n//! import silk.effect { Effect }\n//!\n//! struct Problem {\n//! answer: i32\n//! }\n//!\n//! effect fn load() -> i32\n//! ! Problem {\n//! fail Problem {answer: 42}\n//! }\n//!\n//! effect fn recover(error: Problem) -> i32 {\n//! return error.answer\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(load(), recover)\n//! }\n//! ```\n\n// Familiar channel transformations derived from the closed compiler core. The core catches typed\n// failures and binds typed requirements; everything here is ordinary Silk.\n\nimport silk.bool as bool\nimport silk.logger { LogError, LogLevel, Logger }\nimport silk.result { Result, failResult, succeed }\nimport silk.usize as usize\n\n/// The importable name of the `silk.effect` module scope.\n///\n/// # Details\n///\n/// This struct carries no data and is never constructed by the library. Importing it as\n/// `import silk.effect { Effect }` names the module scope, so `Effect.map(...)` and every other\n/// combinator resolve through it exactly as through a module alias. It is unrelated to the builtin\n/// `Effect` type, which needs no import.\npub struct Effect {}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The logger decides where the message goes. Logging may fail with [`LogError`], and this wrapper\n/// neither buffers nor recovers that failure. Use [`logAt`] when the level is not Info.\npub effect fn log(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `level` through the required mutable [`Logger`].\n///\n/// # Details\n///\n/// The message is one logging event rather than a fragment. The provider controls formatting and\n/// destination; its [`LogError`] propagates unchanged.\npub effect fn logAt(\n level: LogLevel,\n message: string\n) -> () ! LogError ? &mut Logger {\n return run Logger.log(move level, message)\n}\n\n/// Sends one complete message at `LogLevel.Trace` through the required mutable [`Logger`].\npub effect fn logTrace(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Trace, message)\n}\n\n/// Sends one complete message at `LogLevel.Debug` through the required mutable [`Logger`].\npub effect fn logDebug(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Debug, message)\n}\n\n/// Sends one complete message at `LogLevel.Info` through the required mutable [`Logger`].\npub effect fn logInfo(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Info, message)\n}\n\n/// Sends one complete message at `LogLevel.Warning` through the required mutable [`Logger`].\npub effect fn logWarning(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Warning, message)\n}\n\n/// Sends one complete message at `LogLevel.Error` through the required mutable [`Logger`].\npub effect fn logError(\n message: string\n) -> () ! LogError ? &mut Logger {\n return run logAt(LogLevel.Error, message)\n}\n\n/// Executes `protected` once and converts its success or typed failure into ordinary [`Result`] data.\n///\n/// # Details\n///\n/// The returned Effect still requires `R`, because conversion does not provide services. Its typed\n/// failure row is empty: an `E` becomes [`Failure`] data instead of propagating. Traps are not typed\n/// failures and therefore are not captured.\n///\n/// # Examples\n/// ## Inspect a failure as ordinary data\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// import silk.result as Result\n///\n/// struct Problem {\n/// answer: i32\n/// }\n///\n/// effect fn load() -> i32\n/// ! Problem {\n/// fail Problem {answer: 42}\n/// }\n///\n/// pub fn main() -> i32 {\n/// let completed = run Effect.result(load())\n/// return match move completed {\n/// Result.Result.Success {value} => value\n/// Result.Result.Failure {error} => error.answer\n/// }\n/// }\n/// ```\npub effect fn result(\n protected: once Effect\n) -> Result ? R {\n let succeeded = map, E>(move protected, succeedCompleted)\n return run catchAll, Result, E, never>(move succeeded, failCompleted)\n}\n\neffect fn raise(error: E) -> never ! E {\n fail move error\n}\n\nfn succeedCompleted(value: A) -> Result {\n return succeed(move value)\n}\n\neffect fn failCompleted(error: E) -> Result {\n return failResult(move error)\n}\n\n/// Transforms both possible typed outcomes with pure callbacks.\n///\n/// # Details\n///\n/// Exactly one callback runs after `self`: `onSuccess` changes `A` to `B`, while `onFailure` changes\n/// `E` to `F` and re-raises it. Requirements are preserved, and traps bypass both callbacks.\npub effect fn mapBoth(\n self: once Effect,\n onSuccess: once fn(A) -> B,\n onFailure: once fn(E) -> F\n) -> B ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => onSuccess(move success)\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Applies a pure callback to success while preserving typed failure and requirements.\n///\n/// # Details\n///\n/// `onSuccess` runs once only after `self` succeeds. A typed failure propagates without invoking the\n/// callback. Use [`flatMap`] when the callback itself needs an Effect.\npub effect fn map(\n self: once Effect,\n onSuccess: once fn(A) -> B\n) -> B ! E ? R {\n let success = run move self\n return onSuccess(move success)\n}\n\n/// Applies a pure callback to typed failure while preserving success and requirements.\n///\n/// # Details\n///\n/// `onFailure` runs once only when `self` fails, and its returned `F` becomes the new typed failure.\n/// Success bypasses the callback. This changes an error value; use [`catchAll`] to recover to success.\npub effect fn mapError(\n self: once Effect,\n onFailure: once fn(E) -> F\n) -> A ! F ? R {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(onFailure(move error))\n }\n}\n\n/// Runs `self`, then continues its success with an effectful callback.\n///\n/// # Details\n///\n/// The callback is not invoked when `self` fails. Its failure and requirement rows join those of\n/// `self`, and its success becomes the overall success. This is the general sequencing combinator;\n/// use direct `run` statements when named intermediate values are clearer.\npub effect fn flatMap(\n self: once Effect,\n onSuccess: once fn(A) -> Effect\n) -> B ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run onSuccess(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs an outer Effect and then the inner Effect it produces.\n///\n/// # Details\n///\n/// If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two\n/// requirement rows are joined. `flatten(nested)` is the nested-Effect form of [`flatMap`].\npub effect fn flatten(\n self: once Effect ! E ? R>\n) -> A ! E | F ? R | S {\n let inner = run self\n return run inner\n}\n\n/// Two success values collected in operand order by [`zip`].\npub struct Pair {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n}\n\n/// Three success values collected in operand order by [`zip3`].\npub struct Triple {\n /// The first Effect's success value.\n pub first: A\n /// The second Effect's success value.\n pub second: B\n /// The third Effect's success value.\n pub third: C\n}\n\n/// Runs two Effects in declaration order and collects both success values.\n///\n/// # Details\n///\n/// `self` runs first. Only after it succeeds does `other` run, so a first-step typed failure skips\n/// the second step. Both failure and requirement rows are joined. Use the public `Pair.first` and\n/// `Pair.second` fields to read the results; this is sequencing, not parallel execution.\npub effect fn zip(\n self: once Effect,\n other: once Effect\n) -> Pair ! E | F ? R | S {\n let first = run self\n let second = run other\n return Pair { first: move first, second: move second }\n}\n\n/// Runs three Effects in declaration order and collects all three success values.\n///\n/// # Details\n///\n/// The operands run from left to right. Each later operand is skipped if an earlier one fails, and\n/// all three failure and requirement rows are joined. Use this fixed-arity operation when all three\n/// successful values are needed together; it does not run them concurrently.\npub effect fn zip3(\n self: once Effect,\n second: once Effect,\n third: once Effect\n) -> Triple ! E | F | G ? R | S | T {\n let firstValue = run self\n let secondValue = run second\n let thirdValue = run third\n return Triple {\n first: move firstValue,\n second: move secondValue,\n third: move thirdValue\n }\n}\n\n/// Continues success with a callback that returns the value to expose as the overall success.\n///\n/// # Details\n///\n/// The callback receives and consumes the original `A`, then must produce an `A` of its own. This is\n/// useful for effectful observation followed by returning the observed value, but it does not\n/// automatically preserve the original value. A failure from either step propagates, and the\n/// callback is skipped when `self` fails.\npub effect fn tap(\n self: once Effect,\n callback: once fn(A) -> Effect\n) -> A ! E | F ? R | S {\n let completed = run result(move self)\n return match move completed {\n Result.Success { value: success } => run callback(move success)\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Recovers every typed failure in the protected row with another Effect.\n///\n/// # Details\n///\n/// The handler receives the complete failure value and runs only on typed failure. The protected\n/// failure row is removed in full; only the handler's own `F` can fail afterwards. Success bypasses\n/// the handler, requirements from both paths remain, and traps are not recovered. Use [`catch`] to\n/// handle one selected member while leaving the other failures in the row.\npub effect fn catchAll(\n self: once Effect,\n onFailure: once fn(E) -> Effect\n) -> A | B ! F ? R | S {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Recovers one selected typed failure.\n///\n/// # Details\n///\n/// `Effect.catch(protected, handler)` names one member of the protected row. The handler runs\n/// only for that member, its own failures join the result row, and every nonmatching member of\n/// the protected row propagates unchanged as the residual. Success bypasses the handler.\n///\n/// A success bypasses the handler. A matching `S` invokes it once; nonmatching typed failures\n/// propagate in `Without`, and the handler's failures join as `F`. Requirements from either\n/// path remain. Traps are not selected or recovered. Use [`catchAll`] when the handler should receive\n/// the entire failure value regardless of its union member.\npub effect fn catch(\n self: once Effect,\n onFailure: once fn(S) -> Effect\n) -> A | B ! Without | F ? R | Q\nwhere S in E {\n return run Intrinsic.catchFailure(move self, move onFailure)\n}\n\n/// Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.\n///\n/// # Details\n///\n/// The protected Effect is converted into Result data before the finalizer runs, which is what fixes\n/// the order: a typed failure reaches this body as data rather than as a propagation, so the\n/// protected Effect's own frame — and every local it cleans up — is already gone by the time the\n/// finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the\n/// cleanup it wraps. The original success value or the original typed failure is only handed on\n/// afterwards, so a recovering caller never observes the outcome before the finalizer has run.\n///\n/// The finalizer is typed `! never`: it cannot fail, so there is no second outcome to reconcile\n/// with the one being preserved. A caller with fallible cleanup recovers it into `! never` first\n/// — for example with `Effect.catch` — and decides there what a failed release means.\n///\n/// A trap is not an outcome. It bypasses the finalizer exactly as it bypasses `Effect.catch` and\n/// every Drop hook.\npub effect fn ensuring(\n self: once Effect,\n finalizer: once Effect<() ! never ? S>\n) -> A ! E ? R | S {\n let completed = run result(move self)\n let finalized = run move finalizer\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Runs exactly one of two suspended branches, selected by a condition.\n///\n/// # Details\n///\n/// The arms are suspended rather than pre-built: each is a `once fn()` that produces its branch's\n/// Effect, and only the selected arm is invoked. The branch not taken is therefore never\n/// constructed, which is a stronger guarantee than merely not being run — construction-time work\n/// inside an arm never happens, and an arm whose body is only well-defined under the condition is\n/// safe to write. Two pre-built `Effect` arguments would instead be evaluated at the call site,\n/// before either was chosen.\n///\n/// The unselected arm is released here with an explicit `drop move`, so the affine obligation for\n/// the arm that is never invoked is discharged in this source rather than left to a generated\n/// release.\n///\n/// The result's failure and requirement rows are the union of the two arms', so the caller\n/// discharges whatever either branch could need without knowing which one will be selected. Both\n/// arms must agree on the success type.\n///\n/// The name is `ifThenElse` rather than `if` because `if` is a keyword and Silk has no\n/// raw-identifier form, so the declaration itself could not be spelled `if`.\npub effect fn ifThenElse(\n condition: bool,\n onTrue: once fn() -> Effect,\n onFalse: once fn() -> Effect\n) -> A ! E | F ? R | S {\n if condition {\n drop move onFalse\n return run onTrue()\n }\n drop move onTrue\n return run onFalse()\n}\n\neffect fn retryFailure(\n self: mut Effect,\n error: E,\n retries: usize\n) -> A ! E ? R {\n if retries == 0 {\n return run raise(move error)\n }\n return run retryLoop(self, retries - 1)\n}\n\neffect fn retryLoop(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n let completed = run result(self)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run retryFailure(self, move error, retries)\n }\n}\n\n/// Runs a reusable Effect once, then repeats it after typed failure up to `retries` more times.\n///\n/// # Details\n///\n/// Success stops the loop immediately. If every attempt fails, the final typed failure propagates.\n/// `retries == 0` means one initial attempt. Traps are not retried, and `self` must be reusable\n/// (`mut Effect`) because the same computation may execute more than once.\npub effect fn retry(\n self: mut Effect,\n retries: usize\n) -> A ! E ? R {\n return run retryLoop(self, retries)\n}\n\n/// Satisfies one exact shared service requirement with a provider borrowed for this execution.\n///\n/// # Details\n///\n/// The selected row `S` is the first generic argument. Selection may use exact capability identity\n/// or one unique service-conformance witness, but a shared provider selects only a stored shared\n/// requirement. Subtraction removes that exact stored capability-role-access member. The borrow is\n/// lexical: the provider remains owned by the caller after the Effect completes.\npub effect fn bindRequirement(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n let bound = Intrinsic.bindRequirement(move self, provider)\n return run bound\n}\n\n/// Satisfies one service requirement with a provider borrowed exclusively for this execution.\n///\n/// # Details\n///\n/// An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is\n/// still the exact stored member, so providing `&mut P` for a shared `&Logger` removes `&Logger`, not\n/// a synthesized `&mut Logger`. The caller regains exclusive access after the Effect completes.\npub effect fn bindRequirementMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\n/// Satisfies one typed service requirement by taking ownership of its provider.\n///\n/// # Details\n///\n/// Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes\n/// the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains\n/// repeatable. The provider is released with the Effect's lexical scope; it is not returned.\npub effect fn bindRequirementOwned(\n self: once Effect,\n provider: P\n) -> A ! E ? Without\nwhere P provides S from R {\n let bound = Intrinsic.bindRequirementOwned(move self, move provider)\n return run bound\n}\n\n/// Provides a shared service for one lexical Effect execution.\n///\n/// # Details\n///\n/// This is the user-facing alias of [`bindRequirement`]. The provider is borrowed, the exact selected\n/// shared row member is removed, and every unrelated requirement remains visible in the return type.\npub effect fn provide(\n self: once Effect,\n provider: &P\n) -> A ! E ? Without\nwhere &P provides S from R {\n return run bindRequirement(move self, provider)\n}\n\n/// Provides a service from an exclusive borrow for one lexical Effect execution.\n///\n/// # Details\n///\n/// Selection scans the whole input row and subtracts the exact stored member selected by provider\n/// identity or one unique conformance witness. Canonical row order is never selection evidence.\n/// Supply the selected row first when one provider could satisfy multiple entries. The provider is\n/// not moved and becomes exclusively available to the caller again after execution.\n///\n/// # Examples\n///\n/// ## Mutate a custom service for one computation\n///\n/// ```silk\n/// import silk.effect { Effect }\n///\n/// service Counter {\n/// effect fn next() -> i32 ? &mut Counter\n/// }\n///\n/// struct Counting {\n/// value: i32\n/// }\n///\n/// effect fn next(self: &mut Counting) -> i32 {\n/// self.value = self.value + 1\n/// return self.value\n/// }\n///\n/// impl Counter for Counting {\n/// next: Counting.next\n/// }\n///\n/// effect fn read() -> i32\n/// ? &mut Counter {\n/// return run Counter.next()\n/// }\n///\n/// pub fn main() -> i32 {\n/// let mut counter = Counting {value: 41}\n/// return run Effect.provideMut(read(), &mut counter)\n/// }\n/// ```\npub effect fn provideMut(\n self: once Effect,\n provider: &mut P\n) -> A ! E ? Without\nwhere &mut P provides S from R {\n let bound = Intrinsic.bindRequirementMut(move self, provider)\n return run bound\n}\n\neffect fn acquireProvider(\n self: once Effect,\n acquire: Effect

\n) -> Result ! F ? Without | Q\nwhere &mut P provides S from R {\n let mut provider = run acquire\n let bound = Intrinsic.bindRequirementMut(result(move self), &mut provider)\n return run bound\n}\n\n/// Acquires and lexically provides one typed service requirement.\n///\n/// # Details\n///\n/// `acquire` runs on every execution, and its `F` failures propagate before `self` begins. A\n/// successful provider is borrowed exclusively while `self` runs and is released before either\n/// `self`'s success or typed failure becomes observable to the caller. Retrying the returned Effect\n/// therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements\n/// `Q` and every requirement in `R` except the selected entry `S`.\npub effect fn provideEffect(\n self: once Effect,\n acquire: Effect

\n) -> A ! E | F ? Without | Q\nwhere &mut P provides S from R {\n let completed = run acquireProvider(move self, acquire)\n return match move completed {\n Result.Success { value: success } => move success\n Result.Failure { error } => run raise(move error)\n }\n}\n\n/// Defers one Effect through stack-safe execution while preserving its channels exactly.\n///\n/// # Details\n///\n/// Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames\n/// belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a\n/// recursive or deeply chained boundary that must yield through the stack-safe Effect executor;\n/// ordinary laziness alone does not require suspension.\npub effect fn suspend(\n deferred: once Effect\n) -> A ! E ? R {\n return run Intrinsic.suspendEffect(move deferred)\n}\n\n/// Constructs an Effect that succeeds with the captured value when run.\n///\n/// # Details\n///\n/// Calling `of` evaluates and transfers `value` immediately as an ordinary function argument, but\n/// the returned Effect does not produce that value until execution. The Effect has no typed failure\n/// or requirement channels. For an affine value, constructing the Effect transfers ownership into\n/// it, so that Effect can be consumed only once.\npub effect fn of(value: A) -> A {\n return move value\n}\n", }, { module: 'silk/execution', diff --git a/packages/compiler/stdlib/silk/effect.silk b/packages/compiler/stdlib/silk/effect.silk index c2615e32d..922e9c8a7 100644 --- a/packages/compiler/stdlib/silk/effect.silk +++ b/packages/compiler/stdlib/silk/effect.silk @@ -226,10 +226,6 @@ pub effect fn logError( /// } /// } /// ``` -/// -/// This is ordinary Silk composition: success is mapped into `Result.Success`, then `catchAll` -/// maps the complete typed failure value into `Result.Failure`. Compound failure unions and -/// requirements are preserved, and the exact `once fn` adapters transfer affine payloads once. pub effect fn result( protected: once Effect ) -> Result ? R { From 9917dbad6ba1fa45f556724c7dc9bdd5065cb7dc Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 18:52:41 -0300 Subject: [PATCH 23/42] fix(compiler): align inline catch suspension control --- packages/compiler/src/MirVerification.ts | 8 ++++- packages/compiler/src/ProvisionalMir.ts | 32 ++++++++++++++++--- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/test/UserServices.test.ts | 17 +++++++--- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 6347f7ae8..5be9c25ef 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -2450,7 +2450,13 @@ export const verify = (self: Module): ReadonlyArray => { region.runner.classification === 'Unknown' ) return [] - return !originReachable.has(instanceText(fn.instance)) + const declaration = region.runner.declaration + return declaration === undefined || + !self.functions.some( + (candidate) => + originReachable.has(instanceText(candidate.instance)) && + matchesInstance(candidate, declaration, region.runner.typeArguments), + ) ? [Object.freeze({ fn, region })] : [] }), diff --git a/packages/compiler/src/ProvisionalMir.ts b/packages/compiler/src/ProvisionalMir.ts index 260787074..cc95a0048 100644 --- a/packages/compiler/src/ProvisionalMir.ts +++ b/packages/compiler/src/ProvisionalMir.ts @@ -720,6 +720,23 @@ const deferredOf = ( : undefined } +const effectCatchOf = ( + expression: Hir.Expression, + context: BuildContext, + resolving: ReadonlySet = new Set(), +): Extract | undefined => { + if (expression._tag === 'BindingReference') { + const ordinal = expression.binding.ordinal + if (resolving.has(ordinal)) return undefined + const initializer = context.bindings.get(ordinal) + return initializer === undefined + ? undefined + : effectCatchOf(initializer, context, new Set(resolving).add(ordinal)) + } + if (expression._tag === 'Move') return effectCatchOf(expression.subject, context, resolving) + return expression._tag === 'EffectCatch' ? expression : undefined +} + const catchHandlerRunner = ( expression: Extract, context: BuildContext, @@ -771,6 +788,7 @@ const controlsOfCatch = ( expression: Extract, execution: ExecutionKey, context: BuildContext, + ordinalOffset = 0, ): ReadonlyArray => { const regions: Array = [] if (expression.protected._tag === 'Unavailable') return Object.freeze(regions) @@ -781,8 +799,8 @@ const controlsOfCatch = ( const protectedRunner = runnerOf(expression.protected, context) const protectedPolicy = reifyPolicy(protectedRunner.outcome, context) if (protectedRunner.classification !== 'Synchronous' && protectedPolicy !== undefined) { - const id = controlId(execution, expression.span, 0, 'Invoke') - const complete = controlId(execution, expression.span, 0, 'Complete') + const id = controlId(execution, expression.span, ordinalOffset, 'Invoke') + const complete = controlId(execution, expression.span, ordinalOffset, 'Complete') regions.push( Object.freeze({ _tag: 'ProvisionalRegion', @@ -826,8 +844,8 @@ const controlsOfCatch = ( outcome: handlerRunner.outcome, failureMappings: Object.freeze(mappings), }) - const id = controlId(execution, expression.span, 1, 'Invoke') - const complete = controlId(execution, expression.span, 1, 'Complete') + const id = controlId(execution, expression.span, ordinalOffset + 1, 'Invoke') + const complete = controlId(execution, expression.span, ordinalOffset + 1, 'Complete') regions.push( Object.freeze({ _tag: 'ProvisionalRegion', @@ -873,6 +891,12 @@ const controlsOf = ( if (expression._tag === 'Run') { const idOrdinal = ordinal ordinal += 1 + const caught = effectCatchOf(expression.subject, context) + if (caught !== undefined) { + regions.push(...controlsOfCatch(caught, execution, context, idOrdinal)) + ordinal += 1 + return + } if (isSuspendOrigin(expression.subject, context)) { const deferred = deferredOf(expression.subject, context) if (deferred !== undefined) { diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 51fa8178c..a23a5b953 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '72dbac967a0902952bbe4c39a97e5c2e9bdd57a588b479a7bf3308583216d61a' +export const compilerDigest = '1e87e158465ff7f31e96dc639b2f31299bc88a7fed01e85fb52494c11050afb3' diff --git a/packages/compiler/test/UserServices.test.ts b/packages/compiler/test/UserServices.test.ts index 1b5156f85..7855e4cf5 100644 --- a/packages/compiler/test/UserServices.test.ts +++ b/packages/compiler/test/UserServices.test.ts @@ -617,13 +617,11 @@ pub fn main() -> i32 { let ${provider.access === 'Exclusive' ? 'mut ' : ''}secondToken = Token { value: 0 } let mut secondCell = Cell { value: ${provider.access === 'Exclusive' ? '30' : '31'} } - let secondBound = ${provider.call}(read(${provider.access === 'Exclusive' ? '&mut secondToken' : '&secondToken'}), ${provider.access === 'Exclusive' ? '&mut secondCell' : '&secondCell'}) + let secondReified = Effect.result(read(${provider.access === 'Exclusive' ? '&mut secondToken' : '&secondToken'})) + let secondBound = ${provider.call}(move secondReified, ${provider.access === 'Exclusive' ? '&mut secondCell' : '&secondCell'}) let secondHop = move secondBound let secondAlias = move secondHop - let reified = Effect.result(move secondAlias) - let reifiedHop = move reified - let reifiedAlias = move reifiedHop - let completed = run move reifiedAlias + let completed = run move secondAlias let second = match move completed { Result.Success { value: answer } => answer Result.Failure { error: impossible } => 0 @@ -735,6 +733,15 @@ it.effect('releases an affine owned provider after a pre-read scalar suspends an Effect.gen(function* () { const self = yield* snapshot(ownedProviderSuspendedFailure, 'wasm32-unknown-unknown') assert.deepEqual(Analysis.diagnostics(self), []) + const catchRunner = Analysis.loweredMir(self).functions.find((fn) => + fn.id.name.startsWith('catchAll$effect$'), + ) + const caught = catchRunner?.suspension?.regions.find( + (region) => + region._tag === 'RunSuspendableEffectRegion' && region.completion._tag === 'Reify', + ) + assert.isDefined(caught) + assert.strictEqual(caught?.operation._tag, 'CatchEffect') const outcome = Analysis.evaluate(self) assert.strictEqual( outcome._tag, From a59b9dac7ae5c1eb9cf29756fb06bd9ba2c3bad5 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 19:33:49 -0300 Subject: [PATCH 24/42] fix(compiler): migrate union-sensitive fixtures --- examples/language-pressure/lexer/main.silk | 2 ++ packages/compiler/src/DeclarationFacts.ts | 21 +++++++++++++--- packages/compiler/src/Stdlib.generated.ts | 4 ++-- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/stdlib/silk/filesystem.silk | 17 ++++++++++--- .../compiler/test/ExternalWakeParking.test.ts | 4 ++-- .../test/HashedCollectionPrivilege.test.ts | 6 ++--- .../compiler/test/HashedCollections.test.ts | 24 +++++++++---------- packages/compiler/test/LexerPressure.test.ts | 2 +- .../test/StoredCallableDiagnostic.test.ts | 6 ++--- packages/compiler/test/support/corpus.ts | 14 +++++------ 11 files changed, 65 insertions(+), 37 deletions(-) diff --git a/examples/language-pressure/lexer/main.silk b/examples/language-pressure/lexer/main.silk index aaecc612b..5833d82f4 100644 --- a/examples/language-pressure/lexer/main.silk +++ b/examples/language-pressure/lexer/main.silk @@ -23,6 +23,7 @@ const tokenPipePipe: u8 = 74 const tokenCharLiteral: u8 = 75 const tokenRole: u8 = 76 const tokenEnum: u8 = 77 +const tokenUnion: u8 = 78 struct Token { kind: u8 @@ -206,6 +207,7 @@ fn keywordKind(source: &[u8], start: usize, end: usize) -> u8 { if matches(source, start, end, b"interface") { return tokenInterface } if matches(source, start, end, b"role") { return tokenRole } if matches(source, start, end, b"enum") { return tokenEnum } + if matches(source, start, end, b"union") { return tokenUnion } return tokenIdentifier } diff --git a/packages/compiler/src/DeclarationFacts.ts b/packages/compiler/src/DeclarationFacts.ts index a57a8b221..0286f5737 100644 --- a/packages/compiler/src/DeclarationFacts.ts +++ b/packages/compiler/src/DeclarationFacts.ts @@ -1839,19 +1839,34 @@ export const storedCallable = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return undefined + if (declaration?._tag !== 'StructDeclaration' && declaration?._tag !== 'UnionDeclaration') + return undefined const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), type.arguments, ) ?? new Map() const next = new Set(seen).add(key) - for (const field of declaration.fields) { + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields.map((field) => Object.freeze({ field, path: Object.freeze([]) })) + : declaration.variants.flatMap((variant) => + variant.fields.map((field) => + Object.freeze({ + field, + path: + variant.name._tag === 'Present' + ? Object.freeze([variant.name.spelling]) + : Object.freeze([]), + }), + ), + ) + for (const { field, path } of fields) { if (field.declaredType._tag !== 'Resolved' || field.name._tag !== 'Present') continue const found = storedCallable(self, Type.substitute(field.declaredType.type, substitution), next) if (found !== undefined) return Object.freeze({ - path: Object.freeze([field.name.spelling, ...found.path]), + path: Object.freeze([...path, field.name.spelling, ...found.path]), callable: found.callable, }) } diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index ea8845157..030dedf6c 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -255,7 +255,7 @@ export const modules = [ module: 'silk/filesystem', path: 'silk/filesystem.silk', sourceIdentity: 'silk/filesystem', - digest: 'b533b1c131e6829dc2be296bf4854a90c1df94f158bae237e584c909609b7668', + digest: '7916f77e683a0b40554448fcfcf0f4c4c0fa465465b119875b1e23a77ca6850f', documentation: 'silk/filesystem.silk', layer: 'portable', runtimeInventory: ['replace', 'stringFromUtf8Unchecked'], @@ -271,7 +271,7 @@ export const modules = [ 'Path', ], source: - '//! Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.\n//!\n//! # When to use\n//! Build provider-absolute [`Path`] values with [`make`] or [`fromBytes`], then run operations\n//! through a supplied [`FileSystem`]. Use [`rawBytes`] for platform values that must round-trip even\n//! when they are not UTF-8, and [`resolve`] for lexical relative-path resolution.\n//!\n//! # Details\n//! Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and\n//! embedded `.` or `..`. Resolution handles relative dot components but rejects escape above root.\n//! Directory listings return independently owned child paths in deterministic path-byte order.\n//! Portable [`FileError`] data names both the operation and a closed recovery reason, with an\n//! optional provider code for diagnostics.\n//!\n//! Temporary directories have an explicit lifecycle because removal can fail and needs services.\n//! Use [`release`] when cleanup failure matters, or [`releaseIgnored`] as an infallible finalizer\n//! only after deliberately accepting that loss.\n//!\n//! # Gotchas\n//! A path created from arbitrary bytes may not have a valid text view. Keep using [`rawBytes`] unless\n//! the bytes were validated as UTF-8; [`view`] and [`name`] rely on that caller knowledge.\n//!\n//! # Examples\n//! ## Construct and inspect a portable path\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as FileSystem\n//!\n//! effect fn example() -> i32\n//! ! FileSystem.FileError | Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let path = run FileSystem.make("/workspace")\n//! |> Effect.provideMut(&mut allocator)\n//! if FileSystem.name(&path) == "workspace" {\n//! return 42\n//! }\n//! return 0\n//! }\n//!\n//! effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(example(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n append as bytesAppend,\n asSlice as bytesAsSlice\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string {\n InvalidUtf8,\n fromUtf8 as stringFromUtf8,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n length as vectorLength,\n make as vectorMake,\n pop as vectorPop\n}\n\n/// An owned, normalized absolute path in a [`FileSystem`] provider\'s portable namespace.\n///\n/// # Details\n///\n/// Portable `/` means the selected provider\'s root, not necessarily the host operating system\'s\n/// root. Construct paths through [`make`], [`fromBytes`], [`root`], [`join`], or [`resolve`]; the\n/// representation is private so every `Path` satisfies the normalization rules.\npub struct Path {\n bytes: Bytes\n nameBytes: Bytes\n}\n\n/// Minimal portable metadata for one regular file.\npub struct FileInfo {\n /// Complete file length in bytes.\n pub byteLength: usize\n}\n\n/// Portable metadata identifying a directory; no platform-specific fields are exposed.\npub struct DirectoryInfo {}\n\n/// The closed portable kind of one directory entry.\npub struct DirectoryEntryKind {\n /// Stable portable kind code selected by [`file`] or [`directory`].\n pub code: i32\n}\n\n/// One immediate directory child with an independently owned complete [`Path`].\npub struct DirectoryEntry {\n /// Independently owned complete path to the child.\n pub path: Path\n /// Portable kind reported for the child.\n pub kind: DirectoryEntryKind\n}\n\n/// The stable portable operation category stored in a [`FileError`].\npub struct FileOperation {\n /// Stable code identifying the attempted portable operation.\n pub code: i32\n}\n\n/// A stable portable recovery category stored in a [`FileError`].\npub struct FileReason {\n /// Stable code identifying the portable recovery reason.\n pub code: i32\n}\n\n/// An allocation-free portable failure naming the attempted operation and recovery reason.\n///\n/// # Details\n///\n/// Match or compare [`operationCode`] and [`reasonCode`] for portable recovery. [`providerCode`]\n/// may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions\n/// must not depend on it.\npub struct FileError {\n /// The operation that failed.\n pub operation: FileOperation\n /// The portable reason callers can recover by.\n pub reason: FileReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Constructs the regular-file [`DirectoryEntryKind`].\npub fn file() -> DirectoryEntryKind { return DirectoryEntryKind { code: 0 } }\n\n/// Constructs the directory [`DirectoryEntryKind`].\npub fn directory() -> DirectoryEntryKind { return DirectoryEntryKind { code: 1 } }\n\n/// Returns the stable code for a consumed [`DirectoryEntryKind`]: `0` for file, `1` for directory.\npub fn entryKindCode(kind: DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Reads the stable directory-entry kind code through a borrow.\nfn borrowedKindCode(kind: &DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Constructs regular-file metadata with the complete length in bytes.\npub fn fileInfo(byteLength: usize) -> FileInfo {\n return FileInfo { byteLength: byteLength }\n}\n\n/// Constructs the fieldless portable directory metadata value.\npub fn directoryInfo() -> DirectoryInfo { return DirectoryInfo {} }\n\n/// Constructs a directory entry by taking ownership of its complete child `path` and `kind`.\npub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntry {\n return DirectoryEntry { path: move path, kind: move kind }\n}\n\n/// Selects the read-file operation.\npub fn readFileOperation() -> FileOperation { return FileOperation { code: 0 } }\n\n/// Selects the write-file operation.\npub fn writeFileOperation() -> FileOperation { return FileOperation { code: 1 } }\n\n/// Selects the stat operation.\npub fn statOperation() -> FileOperation { return FileOperation { code: 2 } }\n\n/// Selects the list-directory operation.\npub fn listDirectoryOperation() -> FileOperation { return FileOperation { code: 3 } }\n\n/// Selects the create-directory operation.\npub fn createDirectoryOperation() -> FileOperation { return FileOperation { code: 4 } }\n\n/// Selects the remove-file operation.\npub fn removeFileOperation() -> FileOperation { return FileOperation { code: 5 } }\n\n/// Selects the remove-directory operation.\npub fn removeDirectoryOperation() -> FileOperation { return FileOperation { code: 6 } }\n\n/// Selects path construction and resolution.\npub fn pathOperation() -> FileOperation { return FileOperation { code: 7 } }\n\n/// Selects the create-temporary-directory operation.\npub fn createTemporaryDirectoryOperation() -> FileOperation { return FileOperation { code: 8 } }\n\n/// Returns the stable numeric code of a consumed [`FileOperation`].\npub fn operationCode(operation: FileOperation) -> i32 { return operation.code }\n\n/// Constructs the `NotFound` recovery reason.\npub fn notFound() -> FileReason { return FileReason { code: 0 } }\n\n/// Constructs the `AlreadyExists` recovery reason.\npub fn alreadyExists() -> FileReason { return FileReason { code: 1 } }\n\n/// Constructs the `PermissionDenied` recovery reason.\npub fn permissionDenied() -> FileReason { return FileReason { code: 2 } }\n\n/// Constructs the `InvalidPath` recovery reason.\npub fn invalidPath() -> FileReason { return FileReason { code: 3 } }\n\n/// Constructs the `WrongType` recovery reason.\npub fn wrongType() -> FileReason { return FileReason { code: 4 } }\n\n/// Constructs the `NotEmpty` recovery reason.\npub fn notEmpty() -> FileReason { return FileReason { code: 5 } }\n\n/// Constructs the `NoSpace` recovery reason.\npub fn noSpace() -> FileReason { return FileReason { code: 6 } }\n\n/// Constructs the `TooLarge` recovery reason.\npub fn tooLarge() -> FileReason { return FileReason { code: 7 } }\n\n/// Constructs the `Unsupported` recovery reason.\npub fn unsupported() -> FileReason { return FileReason { code: 8 } }\n\n/// Constructs the catch-all `Other` recovery reason.\npub fn other() -> FileReason { return FileReason { code: 9 } }\n\n/// Returns the stable numeric code of a consumed [`FileReason`].\npub fn reasonCode(reason: FileReason) -> i32 { return reason.code }\n\n/// Constructs a portable [`FileError`] without a provider-specific numeric detail.\npub fn error(operation: FileOperation, reason: FileReason) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false,\n }\n}\n\n/// Constructs a portable [`FileError`] while retaining one provider-specific diagnostic code.\n///\n/// # Details\n///\n/// The numeric `code` is opaque outside that provider. The portable `operation` and `reason` remain\n/// the fields callers should use for recovery.\npub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true,\n }\n}\n\n/// Borrows an error and returns its provider-specific numeric detail, if one was retained.\npub fn providerCode(error: &FileError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\neffect fn raise(error: FileError) -> never ! FileError { fail move error }\n\neffect fn rejectPath() -> never ! FileError {\n fail error(pathOperation(), invalidPath())\n}\n\nfn byte(value: u8) -> i32 { return u8.toI32(value) }\n\nfn containsNul(values: &[u8]) -> bool {\n let mut index = usize.ZERO\n while index < values.length {\n if values[index] == u8.toU8(0) { return true }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validUtf8(values: &[u8]) -> bool {\n let decoded = stringFromUtf8(values)\n return match move decoded {\n Result.Success { value: text } => true\n Result.Failure { error: invalid } => false\n }\n}\n\nfn isDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != usize.ONE { return false }\n return byte(values[start]) == 46\n}\n\nfn isDotDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != 2 { return false }\n if byte(values[start]) != 46 { return false }\n return byte(values[start + usize.ONE]) == 46\n}\n\nfn validAbsolute(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) != 47 { return false }\n if containsNul(values) { return false }\n if values.length == usize.ONE { return true }\n let mut start = usize.ONE\n let mut index = usize.ONE\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validRelativeFragment(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) == 47 { return false }\n if containsNul(values) { return false }\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\neffect fn appendRange(\n target: Bytes,\n source: &[u8],\n start: usize,\n end: usize\n) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = move target\n let mut index = start\n while index < end {\n let one = [source[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\nfn finalNameStart(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ONE }\n let mut index = values.length\n while usize.ZERO < index {\n index = index - usize.ONE\n if byte(values[index]) == 47 { return index + usize.ONE }\n }\n return usize.ZERO\n}\n\neffect fn finishPath(bytes: Bytes) -> Path ! OutOfMemoryError ? &mut Allocator {\n let view = bytesAsSlice(&bytes)\n let start = finalNameStart(view)\n let nameBytes = run appendRange(bytesMake(), view, start, view.length)\n return Path { bytes: move bytes, nameBytes: move nameBytes }\n}\n\n/// Copies UTF-8 text into an owned, normalized provider-absolute [`Path`].\n///\n/// # Details\n///\n/// The text must begin with `/`. Root is valid; every other path must have nonempty components and\n/// no trailing slash, NUL, `.` component, or `..` component. Invalid input fails with\n/// `FileError(pathOperation(), invalidPath())`; copying can fail with [`OutOfMemoryError`].\npub effect fn make(value: string) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let values = stringUtf8Bytes(value)\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Constructs an owned normalized provider-absolute Path from exact platform bytes.\n///\n/// # Details\n///\n/// Platform paths are byte sequences, and a caller that received one from the platform — a\n/// directory entry, an argument, an environment value — must be able to hand it back unchanged.\n/// The same normalization applies as for textual construction: the value is absolute, rejects NUL,\n/// and rejects `.`, `..`, empty components, and trailing separators. Well-formed text is not\n/// required, so a Path built this way may have no `string` view.\npub effect fn fromBytes(values: &[u8]) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Allocates the portable root path `/` in the selected allocator.\npub effect fn root() -> Path ! OutOfMemoryError ? &mut Allocator {\n let mut copied = bytesMake()\n let appended = run bytesAppend(&mut copied, stringUtf8Bytes("/"))\n return run finishPath(move copied)\n}\n\nfn pathBytes(self: &Path) -> &[u8] { return bytesAsSlice(&self.bytes) }\n\n/// Borrows the complete normalized path as exact platform bytes.\n///\n/// # Details\n///\n/// This is the lossless view. It round-trips a Path built from platform bytes even when those\n/// bytes are not well-formed text, which the `string` view cannot promise.\npub fn rawBytes(self: &Path) -> &[u8] {\n return pathBytes(self)\n}\n\n/// Borrows the complete path as text when its bytes are known to be valid UTF-8.\n///\n/// # Details\n///\n/// Paths from [`make`], [`join`], [`joinUtf8`], and [`resolve`] satisfy that precondition. A path\n/// created with [`fromBytes`] may not; use [`rawBytes`] unless the source bytes were validated.\npub fn view(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(pathBytes(self)) }\n return ""\n}\n\n/// Returns `true` exactly when this path is the portable root `/`.\npub fn isRoot(self: &Path) -> bool { return bytesAsSlice(&self.bytes).length == usize.ONE }\n\n/// Borrows the final component as text; root returns empty text.\n///\n/// # Details\n///\n/// This has the same UTF-8 precondition as [`view`]. It does not allocate or include a separator.\npub fn name(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(bytesAsSlice(&self.nameBytes)) }\n return ""\n}\n\neffect fn joinBytes(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validRelativeFragment(fragment) == false { return run rejectPath() }\n let baseBytes = pathBytes(base)\n let mut combined = run appendRange(bytesMake(), baseBytes, usize.ZERO, baseBytes.length)\n if isRoot(base) == false {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeChild = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeChild, fragment, usize.ZERO, fragment.length)\n return run finishPath(move combined)\n}\n\n/// Appends one normalized relative text fragment to an absolute base path.\n///\n/// # Details\n///\n/// `fragment` must be nonempty and relative, with no NUL, empty, `.`, or `..` component and no\n/// trailing slash. Use [`resolve`] when dot components should be interpreted instead of rejected.\npub effect fn join(\n base: &Path,\n fragment: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return run joinBytes(base, stringUtf8Bytes(fragment))\n}\n\n/// Validates UTF-8 bytes as one normalized relative fragment and appends them to `base`.\n///\n/// # Details\n///\n/// This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the\n/// same malformed components rejected by [`join`] fail with the `InvalidPath` reason.\npub effect fn joinUtf8(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validUtf8(fragment) == false { return run rejectPath() }\n return run joinBytes(base, fragment)\n}\n\nfn componentCount(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ZERO }\n let mut count = usize.ONE\n let mut index = usize.ONE\n while index < values.length {\n if byte(values[index]) == 47 { count = count + usize.ONE }\n index = index + usize.ONE\n }\n return count\n}\n\nfn survivingRelative(values: &[u8], after: usize) -> bool {\n let mut depth = usize.ONE\n let mut start = after\n let mut index = after\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDotDot(values, start, index) {\n depth = depth - usize.ONE\n if depth == usize.ZERO { return false }\n } else {\n if isDot(values, start, index) == false { depth = depth + usize.ONE }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return true\n}\n\n/// Resolves relative text lexically against an explicit absolute base.\n///\n/// # Details\n///\n/// Empty text and `.` keep the base; `..` removes components; ordinary components append. An\n/// absolute relative value, an empty interior component, NUL, or any attempt to escape above root\n/// fails with the `InvalidPath` reason. Resolution is lexical and never accesses the filesystem.\npub effect fn resolve(\n base: &Path,\n relativeText: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let relative = stringUtf8Bytes(relativeText)\n if containsNul(relative) { return run rejectPath() }\n if usize.ZERO < relative.length {\n if byte(relative[usize.ZERO]) == 47 { return run rejectPath() }\n }\n let baseBytes = pathBytes(base)\n let mut keptBase = componentCount(baseBytes)\n let mut relativeDepth = usize.ZERO\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start == index {\n if index != relative.length { return run rejectPath() }\n } else {\n if isDotDot(relative, start, index) {\n if usize.ZERO < relativeDepth {\n relativeDepth = relativeDepth - usize.ONE\n } else {\n if keptBase == usize.ZERO { return run rejectPath() }\n keptBase = keptBase - usize.ONE\n }\n } else {\n if isDot(relative, start, index) == false {\n relativeDepth = relativeDepth + usize.ONE\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n let mut combined = bytesMake()\n let rooted = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n let mut included = usize.ZERO\n start = usize.ONE\n index = usize.ONE\n while index <= baseBytes.length {\n let mut boundary = false\n if index == baseBytes.length {\n boundary = true\n } else {\n if byte(baseBytes[index]) == 47 { boundary = true }\n }\n if boundary {\n if included < keptBase {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeBaseComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeBaseComponent, baseBytes, start, index)\n included = included + usize.ONE\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n start = usize.ZERO\n index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDot(relative, start, index) == false {\n if isDotDot(relative, start, index) == false {\n if survivingRelative(relative, index + usize.ONE) {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeRelativeComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeRelativeComponent, relative, start, index)\n included = included + usize.ONE\n }\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return run finishPath(move combined)\n}\n\n/// Allocates an independently owned parent path, or [`None`] when `self` is root.\n///\n/// # Details\n///\n/// The result does not borrow `self`. A direct child of root has root as its parent.\npub effect fn parent(\n self: &Path\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n if isRoot(self) { return none() }\n let values = pathBytes(self)\n let nameStart = finalNameStart(values)\n let mut end = usize.ONE\n if nameStart != usize.ONE { end = nameStart - usize.ONE }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, end)\n let owned = run finishPath(move copied)\n return some(move owned)\n}\n\n/// Portable mutable service for normalized paths and whole-file operations.\n///\n/// # Details\n///\n/// Application code supplies one provider lexically with `Effect.provideMut`; tests can implement\n/// this service in memory, while native applications can use `silk.os_filesystem`. The service owns\n/// platform policy, but every implementation must preserve the portable error categories,\n/// create-or-truncate writes, and deterministic listing order described here.\n///\n/// # Examples\n/// ## Write a file after creating its parents\n/// ```silk\n/// import silk.allocator { Allocator }\n///\n/// import silk.filesystem as FileSystem\n///\n/// import silk.usize as usize\n///\n/// pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize\n/// ! FileSystem.FileError | Allocator.OutOfMemoryError\n/// ? &mut FileSystem.FileSystem | &mut Allocator {\n/// let written = run FileSystem.writeFileWithParents(path, contents)\n/// return contents.length\n/// }\n/// ```\npub service FileSystem {\n /// Reads one complete regular file into independently owned bytes.\n ///\n /// # Details\n ///\n /// Reading a directory fails with `WrongType`. Allocation of the returned [`Bytes`] may fail\n /// independently of the provider read.\n effect fn readFile(\n path: &Path\n ) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Writes one complete byte view with create-or-truncate semantics.\n ///\n /// # Details\n ///\n /// A missing file is created; an existing regular file is replaced by exactly `bytes`. The call\n /// does not create missing parent directories—use [`writeFileWithParents`] for that workflow.\n effect fn writeFile(path: &Path, bytes: &[u8]) -> () ! FileError ? &mut FileSystem\n /// Returns [`FileInfo`] or [`DirectoryInfo`] for the path without opening file contents.\n ///\n /// # Details\n ///\n /// Missing paths fail with `NotFound`; providers use `WrongType` only when an operation requires a\n /// particular kind, not for this discriminating query.\n effect fn stat(path: &Path) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem\n /// Returns immediate owned children in deterministic complete-path byte order.\n ///\n /// # Details\n ///\n /// The result is not recursive. Each `DirectoryEntry.path` is independently owned and may be\n /// retained after the listing vector is released.\n effect fn listDirectory(\n path: &Path\n ) -> Vector ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Creates exactly one missing directory whose parent already exists.\n ///\n /// # Details\n ///\n /// Existing paths fail with `AlreadyExists`; use [`createDirectoriesRecursively`] to ensure every\n /// missing component.\n effect fn createDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one regular file and fails with `WrongType` for a directory.\n effect fn removeFile(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one empty directory.\n ///\n /// # Details\n ///\n /// A nonempty directory fails with `NotEmpty`; use [`removeDirectoryRecursively`] only when all\n /// descendants are intentionally in scope for removal.\n effect fn removeDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Creates one directory under an existing parent under a name no other caller holds.\n ///\n /// # Details\n ///\n /// The provider chooses the name\'s unique part and returns the complete Path, because only the\n /// provider can create and claim a name in one step. A caller that supplied the name would have\n /// to check-then-create, and the gap between those two is exactly the race this avoids.\n /// `prefix` is a byte prefix for the provider-chosen child name, not a complete path. The returned\n /// directory already exists and is an immediate child of `parent`.\n effect fn createTemporaryDirectory(\n parent: &Path,\n prefix: &[u8]\n ) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n}\n\n/// A directory a caller owns outright, together with everything written inside it.\n///\n/// # Details\n///\n/// Ownership is affine: `TemporaryDirectory` holds an owned `Path`, so exactly one binding holds\n/// it and the compiler rejects a second use of a moved one. Ownership is not, however, a `Drop`\n/// hook. Removing a directory is a fallible operation that requires the `FileSystem` capability,\n/// and a `Drop` hook may carry neither a failure row nor a requirement row, so a hook here could\n/// only be written by inventing an infallible intrinsic over a fallible syscall. Release is\n/// therefore explicit and honest about both rows — see `release`.\n///\n/// Scope ownership comes from composition rather than from a hook: `Effect.ensuring(release)`\n/// runs the release whatever the protected Effect\'s outcome. Because `ensuring` types its\n/// finalizer `! never`, that composition has to say what a failed removal means; `releaseIgnored`\n/// is the stdlib\'s answer and names the loss at the call site.\npub struct TemporaryDirectory {\n /// The complete owned path callers use while the scope remains live.\n pub path: Path\n}\n\n/// Creates an explicitly owned temporary directory under `parent` with a name beginning in `prefix`.\n///\n/// # Details\n///\n/// The result is owned. Nothing removes it until a caller runs [`release`] or [`releaseIgnored`].\n/// The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in\n/// one operation.\npub effect fn temporaryDirectory(\n parent: &Path,\n prefix: string\n) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let created = run FileSystem.createTemporaryDirectory(parent, stringUtf8Bytes(prefix))\n return TemporaryDirectory { path: move created }\n}\n\n/// Consumes one TemporaryDirectory and removes it together with everything inside it.\n///\n/// # Details\n///\n/// Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the\n/// tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a\n/// failed cleanup uses this operation and handles the failure. The owner is consumed even when\n/// removal fails, so copy any diagnostic path information needed before calling.\npub effect fn release(\n self: TemporaryDirectory\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let owned = move self\n let removed = run removeDirectoryRecursively(&owned.path)\n drop owned\n return ()\n}\n\neffect fn discardReleaseFailure(error: FileError | OutOfMemoryError) -> () { return () }\n\n/// Consumes one TemporaryDirectory, removes it, and discards a failed removal.\n///\n/// # Details\n///\n/// This exists because `Effect.ensuring` types its finalizer `! never`, so a fallible release has\n/// to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a\n/// caller reading `releaseIgnored` at the call site can see that a failed removal is being\n/// dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —\n/// a directory the host will reap — and what is kept is the protected Effect\'s own outcome, which\n/// is the answer the program was computing.\n///\n/// A caller who needs the failure uses `release` instead and does not compose it with `ensuring`.\n///\n/// The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer\n/// starts. Derive the required paths before you give the owner to the finalizer.\npub effect fn releaseIgnored(\n self: TemporaryDirectory\n) -> () ? &mut FileSystem | &mut Allocator {\n return run Effect.catchAll(release(move self), discardReleaseFailure)\n}\n\n/// Copies one recorded Path out of the walk\'s own record.\n///\n/// The walk appends to the same record it is reading, so it reads through a copy rather than\n/// through a borrow that the next append would invalidate.\neffect fn recordedCopy(\n recorded: &Vector,\n index: usize\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return match &vectorAsSlice(recorded)[index] {\n Path { bytes, nameBytes } => run fromBytes(bytesAsSlice(&bytes))\n }\n}\n\n/// Removes a directory, every descendant file, and every descendant directory.\n///\n/// # Details\n///\n/// Two passes, because the portable primitive removes exactly one *empty* directory. The first\n/// pass walks the tree front to back, unlinking every file it meets and recording every directory\n/// it meets; the second removes the recorded directories back to front. That order is\n/// child-before-parent for free: a directory is always recorded before the children found inside\n/// it, so reversing the record reverses the containment. Neither pass recurses, so depth costs\n/// vector capacity rather than stack.\n///\n/// This operation is destructive and not transactional. If a provider or allocation failure occurs,\n/// removals already completed remain completed and the remaining tree is left in place.\npub effect fn removeDirectoryRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let mut recorded = vectorMake()\n let seed = run fromBytes(rawBytes(path))\n let noted = run vectorAppend(&mut recorded, move seed)\n let mut index = usize.ZERO\n while index < vectorLength(&recorded) {\n let current = run recordedCopy(&recorded, index)\n let entries = run FileSystem.listDirectory(¤t)\n let listed = vectorAsSlice(&entries)\n let mut cursor = usize.ZERO\n while cursor < listed.length {\n let childKind = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } => borrowedKindCode(&childEntryKind)\n }\n if childKind == 0 {\n let unlinked = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run FileSystem.removeFile(&childPath)\n }\n } else {\n let toRemove = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run fromBytes(rawBytes(&childPath))\n }\n let notedChild = run vectorAppend(&mut recorded, move toRemove)\n }\n cursor = cursor + usize.ONE\n }\n index = index + usize.ONE\n }\n while usize.ZERO < vectorLength(&recorded) {\n let taken = vectorPop(&mut recorded)\n let emptied = match move taken {\n Option.Some { value: selected } => move selected\n Option.None => run fromBytes(rawBytes(path))\n }\n let removed = run FileSystem.removeDirectory(&emptied)\n }\n return ()\n}\n\nstruct DirectoryPresent {}\nstruct DirectoryMissing {}\nstruct DirectoryWrongType {}\nstruct DirectoryStatFailure { error: FileError }\n\nfn classifyStatFailure(\n failure: FileError\n) -> DirectoryMissing | DirectoryStatFailure {\n if failure.reason.code == 0 { return DirectoryMissing {} }\n return DirectoryStatFailure { error: move failure }\n}\n\nfn classifyDirectory(\n outcome: Result\n) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure {\n return match move outcome {\n Result.Success { value: info } => match move info {\n DirectoryInfo {} => DirectoryPresent {}\n FileInfo { byteLength } => DirectoryWrongType {}\n }\n Result.Failure { error: failure } => classifyStatFailure(move failure)\n }\n}\n\n/// Ensures that `path` and every missing ancestor exist as directories.\n///\n/// # Details\n///\n/// Existing directories are kept. An existing regular file at any component fails with\n/// `WrongType`; failures other than `NotFound` propagate. This is ordinary stat-then-create\n/// composition, so concurrent namespace changes may still race according to provider policy.\npub effect fn createDirectoriesRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let values = pathBytes(path)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Effect.result(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return ()\n}\n\n/// Ensures every parent directory exists, then writes the complete byte view to `path`.\n///\n/// # Details\n///\n/// The final write uses `FileSystem.writeFile` create-or-truncate semantics. Passing root delegates\n/// directly to the provider and normally fails with `WrongType`. Directory creation and writing are\n/// not transactional, so a later failure may leave newly created parents behind.\npub effect fn writeFileWithParents(\n path: &Path,\n bytes: &[u8]\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n if isRoot(path) { return run FileSystem.writeFile(path, bytes) }\n let pathValues = pathBytes(path)\n let nameStart = finalNameStart(pathValues)\n let mut parentEnd = usize.ONE\n if nameStart != usize.ONE { parentEnd = nameStart - usize.ONE }\n let parentBytes = run appendRange(bytesMake(), pathValues, usize.ZERO, parentEnd)\n let ownedParent = run finishPath(move parentBytes)\n let values = pathBytes(&ownedParent)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Effect.result(FileSystem.stat(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return run FileSystem.writeFile(path, bytes)\n}\n\neffect fn existsFailure(failure: FileError) -> bool ! FileError {\n if failure.reason.code == 0 { return false }\n return run raise(move failure)\n}\n\n/// Returns whether a file or directory exists at `path`.\n///\n/// # Details\n///\n/// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider\n/// failure propagate so callers cannot mistake an inaccessible path for an absent one.\npub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem {\n let completed = run Effect.result(FileSystem.stat(path))\n return match move completed {\n Result.Success { value: info } => true\n Result.Failure { error: failure } => run existsFailure(move failure)\n }\n}\n', + '//! Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.\n//!\n//! # When to use\n//! Build provider-absolute [`Path`] values with [`make`] or [`fromBytes`], then run operations\n//! through a supplied [`FileSystem`]. Use [`rawBytes`] for platform values that must round-trip even\n//! when they are not UTF-8, and [`resolve`] for lexical relative-path resolution.\n//!\n//! # Details\n//! Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and\n//! embedded `.` or `..`. Resolution handles relative dot components but rejects escape above root.\n//! Directory listings return independently owned child paths in deterministic path-byte order.\n//! Portable [`FileError`] data names both the operation and a closed recovery reason, with an\n//! optional provider code for diagnostics.\n//!\n//! Temporary directories have an explicit lifecycle because removal can fail and needs services.\n//! Use [`release`] when cleanup failure matters, or [`releaseIgnored`] as an infallible finalizer\n//! only after deliberately accepting that loss.\n//!\n//! # Gotchas\n//! A path created from arbitrary bytes may not have a valid text view. Keep using [`rawBytes`] unless\n//! the bytes were validated as UTF-8; [`view`] and [`name`] rely on that caller knowledge.\n//!\n//! # Examples\n//! ## Construct and inspect a portable path\n//! ```silk\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.filesystem as FileSystem\n//!\n//! effect fn example() -> i32\n//! ! FileSystem.FileError | Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let path = run FileSystem.make("/workspace")\n//! |> Effect.provideMut(&mut allocator)\n//! if FileSystem.name(&path) == "workspace" {\n//! return 42\n//! }\n//! return 0\n//! }\n//!\n//! effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(example(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n append as bytesAppend,\n asSlice as bytesAsSlice\n}\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.effect { Effect }\nimport silk.i32 as i32\nimport silk.option { Option, none, some }\nimport silk.result { Result }\nimport silk.string {\n InvalidUtf8,\n fromUtf8 as stringFromUtf8,\n utf8Bytes as stringUtf8Bytes\n}\nimport silk.u8 as u8\nimport silk.usize as usize\nimport silk.vector {\n Vector,\n append as vectorAppend,\n asSlice as vectorAsSlice,\n length as vectorLength,\n make as vectorMake,\n pop as vectorPop\n}\n\n/// An owned, normalized absolute path in a [`FileSystem`] provider\'s portable namespace.\n///\n/// # Details\n///\n/// Portable `/` means the selected provider\'s root, not necessarily the host operating system\'s\n/// root. Construct paths through [`make`], [`fromBytes`], [`root`], [`join`], or [`resolve`]; the\n/// representation is private so every `Path` satisfies the normalization rules.\npub struct Path {\n bytes: Bytes\n nameBytes: Bytes\n}\n\n/// Minimal portable metadata for one regular file.\npub struct FileInfo {\n /// Complete file length in bytes.\n pub byteLength: usize\n}\n\n/// Portable metadata identifying a directory; no platform-specific fields are exposed.\npub struct DirectoryInfo {}\n\n/// The closed portable kind of one directory entry.\npub struct DirectoryEntryKind {\n /// Stable portable kind code selected by [`file`] or [`directory`].\n pub code: i32\n}\n\n/// One immediate directory child with an independently owned complete [`Path`].\npub struct DirectoryEntry {\n /// Independently owned complete path to the child.\n pub path: Path\n /// Portable kind reported for the child.\n pub kind: DirectoryEntryKind\n}\n\n/// The stable portable operation category stored in a [`FileError`].\npub struct FileOperation {\n /// Stable code identifying the attempted portable operation.\n pub code: i32\n}\n\n/// A stable portable recovery category stored in a [`FileError`].\npub struct FileReason {\n /// Stable code identifying the portable recovery reason.\n pub code: i32\n}\n\n/// An allocation-free portable failure naming the attempted operation and recovery reason.\n///\n/// # Details\n///\n/// Match or compare [`operationCode`] and [`reasonCode`] for portable recovery. [`providerCode`]\n/// may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions\n/// must not depend on it.\npub struct FileError {\n /// The operation that failed.\n pub operation: FileOperation\n /// The portable reason callers can recover by.\n pub reason: FileReason\n providerCodeValue: i32\n hasProviderCode: bool\n}\n\n/// Constructs the regular-file [`DirectoryEntryKind`].\npub fn file() -> DirectoryEntryKind { return DirectoryEntryKind { code: 0 } }\n\n/// Constructs the directory [`DirectoryEntryKind`].\npub fn directory() -> DirectoryEntryKind { return DirectoryEntryKind { code: 1 } }\n\n/// Returns the stable code for a consumed [`DirectoryEntryKind`]: `0` for file, `1` for directory.\npub fn entryKindCode(kind: DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Reads the stable directory-entry kind code through a borrow.\nfn borrowedKindCode(kind: &DirectoryEntryKind) -> i32 { return kind.code }\n\n/// Constructs regular-file metadata with the complete length in bytes.\npub fn fileInfo(byteLength: usize) -> FileInfo {\n return FileInfo { byteLength: byteLength }\n}\n\n/// Constructs the fieldless portable directory metadata value.\npub fn directoryInfo() -> DirectoryInfo { return DirectoryInfo {} }\n\n/// Constructs a directory entry by taking ownership of its complete child `path` and `kind`.\npub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntry {\n return DirectoryEntry { path: move path, kind: move kind }\n}\n\n/// Selects the read-file operation.\npub fn readFileOperation() -> FileOperation { return FileOperation { code: 0 } }\n\n/// Selects the write-file operation.\npub fn writeFileOperation() -> FileOperation { return FileOperation { code: 1 } }\n\n/// Selects the stat operation.\npub fn statOperation() -> FileOperation { return FileOperation { code: 2 } }\n\n/// Selects the list-directory operation.\npub fn listDirectoryOperation() -> FileOperation { return FileOperation { code: 3 } }\n\n/// Selects the create-directory operation.\npub fn createDirectoryOperation() -> FileOperation { return FileOperation { code: 4 } }\n\n/// Selects the remove-file operation.\npub fn removeFileOperation() -> FileOperation { return FileOperation { code: 5 } }\n\n/// Selects the remove-directory operation.\npub fn removeDirectoryOperation() -> FileOperation { return FileOperation { code: 6 } }\n\n/// Selects path construction and resolution.\npub fn pathOperation() -> FileOperation { return FileOperation { code: 7 } }\n\n/// Selects the create-temporary-directory operation.\npub fn createTemporaryDirectoryOperation() -> FileOperation { return FileOperation { code: 8 } }\n\n/// Returns the stable numeric code of a consumed [`FileOperation`].\npub fn operationCode(operation: FileOperation) -> i32 { return operation.code }\n\n/// Constructs the `NotFound` recovery reason.\npub fn notFound() -> FileReason { return FileReason { code: 0 } }\n\n/// Constructs the `AlreadyExists` recovery reason.\npub fn alreadyExists() -> FileReason { return FileReason { code: 1 } }\n\n/// Constructs the `PermissionDenied` recovery reason.\npub fn permissionDenied() -> FileReason { return FileReason { code: 2 } }\n\n/// Constructs the `InvalidPath` recovery reason.\npub fn invalidPath() -> FileReason { return FileReason { code: 3 } }\n\n/// Constructs the `WrongType` recovery reason.\npub fn wrongType() -> FileReason { return FileReason { code: 4 } }\n\n/// Constructs the `NotEmpty` recovery reason.\npub fn notEmpty() -> FileReason { return FileReason { code: 5 } }\n\n/// Constructs the `NoSpace` recovery reason.\npub fn noSpace() -> FileReason { return FileReason { code: 6 } }\n\n/// Constructs the `TooLarge` recovery reason.\npub fn tooLarge() -> FileReason { return FileReason { code: 7 } }\n\n/// Constructs the `Unsupported` recovery reason.\npub fn unsupported() -> FileReason { return FileReason { code: 8 } }\n\n/// Constructs the catch-all `Other` recovery reason.\npub fn other() -> FileReason { return FileReason { code: 9 } }\n\n/// Returns the stable numeric code of a consumed [`FileReason`].\npub fn reasonCode(reason: FileReason) -> i32 { return reason.code }\n\n/// Constructs a portable [`FileError`] without a provider-specific numeric detail.\npub fn error(operation: FileOperation, reason: FileReason) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: 0,\n hasProviderCode: false,\n }\n}\n\n/// Constructs a portable [`FileError`] while retaining one provider-specific diagnostic code.\n///\n/// # Details\n///\n/// The numeric `code` is opaque outside that provider. The portable `operation` and `reason` remain\n/// the fields callers should use for recovery.\npub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileError {\n return FileError {\n operation: move operation,\n reason: move reason,\n providerCodeValue: code,\n hasProviderCode: true,\n }\n}\n\n/// Borrows an error and returns its provider-specific numeric detail, if one was retained.\npub fn providerCode(error: &FileError) -> Option {\n if error.hasProviderCode { return some(error.providerCodeValue) }\n return none()\n}\n\neffect fn raise(error: FileError) -> never ! FileError { fail move error }\n\neffect fn rejectPath() -> never ! FileError {\n fail error(pathOperation(), invalidPath())\n}\n\nfn byte(value: u8) -> i32 { return u8.toI32(value) }\n\nfn containsNul(values: &[u8]) -> bool {\n let mut index = usize.ZERO\n while index < values.length {\n if values[index] == u8.toU8(0) { return true }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validUtf8(values: &[u8]) -> bool {\n let decoded = stringFromUtf8(values)\n return match move decoded {\n Result.Success { value: text } => true\n Result.Failure { error: invalid } => false\n }\n}\n\nfn isDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != usize.ONE { return false }\n return byte(values[start]) == 46\n}\n\nfn isDotDot(values: &[u8], start: usize, end: usize) -> bool {\n if end - start != 2 { return false }\n if byte(values[start]) != 46 { return false }\n return byte(values[start + usize.ONE]) == 46\n}\n\nfn validAbsolute(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) != 47 { return false }\n if containsNul(values) { return false }\n if values.length == usize.ONE { return true }\n let mut start = usize.ONE\n let mut index = usize.ONE\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\nfn validRelativeFragment(values: &[u8]) -> bool {\n if values.length == usize.ZERO { return false }\n if byte(values[usize.ZERO]) == 47 { return false }\n if containsNul(values) { return false }\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= values.length {\n if index == values.length {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n return true\n }\n if byte(values[index]) == 47 {\n if start == index { return false }\n if isDot(values, start, index) { return false }\n if isDotDot(values, start, index) { return false }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return false\n}\n\neffect fn appendRange(\n target: Bytes,\n source: &[u8],\n start: usize,\n end: usize\n) -> Bytes ! OutOfMemoryError ? &mut Allocator {\n let mut result = move target\n let mut index = start\n while index < end {\n let one = [source[index]]\n let appended = run bytesAppend(&mut result, &one)\n index = index + usize.ONE\n }\n return move result\n}\n\nfn finalNameStart(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ONE }\n let mut index = values.length\n while usize.ZERO < index {\n index = index - usize.ONE\n if byte(values[index]) == 47 { return index + usize.ONE }\n }\n return usize.ZERO\n}\n\neffect fn finishPath(bytes: Bytes) -> Path ! OutOfMemoryError ? &mut Allocator {\n let view = bytesAsSlice(&bytes)\n let start = finalNameStart(view)\n let nameBytes = run appendRange(bytesMake(), view, start, view.length)\n return Path { bytes: move bytes, nameBytes: move nameBytes }\n}\n\n/// Copies UTF-8 text into an owned, normalized provider-absolute [`Path`].\n///\n/// # Details\n///\n/// The text must begin with `/`. Root is valid; every other path must have nonempty components and\n/// no trailing slash, NUL, `.` component, or `..` component. Invalid input fails with\n/// `FileError(pathOperation(), invalidPath())`; copying can fail with [`OutOfMemoryError`].\npub effect fn make(value: string) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let values = stringUtf8Bytes(value)\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Constructs an owned normalized provider-absolute Path from exact platform bytes.\n///\n/// # Details\n///\n/// Platform paths are byte sequences, and a caller that received one from the platform — a\n/// directory entry, an argument, an environment value — must be able to hand it back unchanged.\n/// The same normalization applies as for textual construction: the value is absolute, rejects NUL,\n/// and rejects `.`, `..`, empty components, and trailing separators. Well-formed text is not\n/// required, so a Path built this way may have no `string` view.\npub effect fn fromBytes(values: &[u8]) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validAbsolute(values) == false { return run rejectPath() }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, values.length)\n return run finishPath(move copied)\n}\n\n/// Allocates the portable root path `/` in the selected allocator.\npub effect fn root() -> Path ! OutOfMemoryError ? &mut Allocator {\n let mut copied = bytesMake()\n let appended = run bytesAppend(&mut copied, stringUtf8Bytes("/"))\n return run finishPath(move copied)\n}\n\nfn pathBytes(self: &Path) -> &[u8] { return bytesAsSlice(&self.bytes) }\n\n/// Borrows the complete normalized path as exact platform bytes.\n///\n/// # Details\n///\n/// This is the lossless view. It round-trips a Path built from platform bytes even when those\n/// bytes are not well-formed text, which the `string` view cannot promise.\npub fn rawBytes(self: &Path) -> &[u8] {\n return pathBytes(self)\n}\n\n/// Borrows the complete path as text when its bytes are known to be valid UTF-8.\n///\n/// # Details\n///\n/// Paths from [`make`], [`join`], [`joinUtf8`], and [`resolve`] satisfy that precondition. A path\n/// created with [`fromBytes`] may not; use [`rawBytes`] unless the source bytes were validated.\npub fn view(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(pathBytes(self)) }\n return ""\n}\n\n/// Returns `true` exactly when this path is the portable root `/`.\npub fn isRoot(self: &Path) -> bool { return bytesAsSlice(&self.bytes).length == usize.ONE }\n\n/// Borrows the final component as text; root returns empty text.\n///\n/// # Details\n///\n/// This has the same UTF-8 precondition as [`view`]. It does not allocate or include a separator.\npub fn name(self: &Path) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(bytesAsSlice(&self.nameBytes)) }\n return ""\n}\n\neffect fn joinBytes(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validRelativeFragment(fragment) == false { return run rejectPath() }\n let baseBytes = pathBytes(base)\n let mut combined = run appendRange(bytesMake(), baseBytes, usize.ZERO, baseBytes.length)\n if isRoot(base) == false {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeChild = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeChild, fragment, usize.ZERO, fragment.length)\n return run finishPath(move combined)\n}\n\n/// Appends one normalized relative text fragment to an absolute base path.\n///\n/// # Details\n///\n/// `fragment` must be nonempty and relative, with no NUL, empty, `.`, or `..` component and no\n/// trailing slash. Use [`resolve`] when dot components should be interpreted instead of rejected.\npub effect fn join(\n base: &Path,\n fragment: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return run joinBytes(base, stringUtf8Bytes(fragment))\n}\n\n/// Validates UTF-8 bytes as one normalized relative fragment and appends them to `base`.\n///\n/// # Details\n///\n/// This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the\n/// same malformed components rejected by [`join`] fail with the `InvalidPath` reason.\npub effect fn joinUtf8(\n base: &Path,\n fragment: &[u8]\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n if validUtf8(fragment) == false { return run rejectPath() }\n return run joinBytes(base, fragment)\n}\n\nfn componentCount(values: &[u8]) -> usize {\n if values.length == usize.ONE { return usize.ZERO }\n let mut count = usize.ONE\n let mut index = usize.ONE\n while index < values.length {\n if byte(values[index]) == 47 { count = count + usize.ONE }\n index = index + usize.ONE\n }\n return count\n}\n\nfn survivingRelative(values: &[u8], after: usize) -> bool {\n let mut depth = usize.ONE\n let mut start = after\n let mut index = after\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDotDot(values, start, index) {\n depth = depth - usize.ONE\n if depth == usize.ZERO { return false }\n } else {\n if isDot(values, start, index) == false { depth = depth + usize.ONE }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return true\n}\n\n/// Resolves relative text lexically against an explicit absolute base.\n///\n/// # Details\n///\n/// Empty text and `.` keep the base; `..` removes components; ordinary components append. An\n/// absolute relative value, an empty interior component, NUL, or any attempt to escape above root\n/// fails with the `InvalidPath` reason. Resolution is lexical and never accesses the filesystem.\npub effect fn resolve(\n base: &Path,\n relativeText: string\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n let relative = stringUtf8Bytes(relativeText)\n if containsNul(relative) { return run rejectPath() }\n if usize.ZERO < relative.length {\n if byte(relative[usize.ZERO]) == 47 { return run rejectPath() }\n }\n let baseBytes = pathBytes(base)\n let mut keptBase = componentCount(baseBytes)\n let mut relativeDepth = usize.ZERO\n let mut start = usize.ZERO\n let mut index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start == index {\n if index != relative.length { return run rejectPath() }\n } else {\n if isDotDot(relative, start, index) {\n if usize.ZERO < relativeDepth {\n relativeDepth = relativeDepth - usize.ONE\n } else {\n if keptBase == usize.ZERO { return run rejectPath() }\n keptBase = keptBase - usize.ONE\n }\n } else {\n if isDot(relative, start, index) == false {\n relativeDepth = relativeDepth + usize.ONE\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n let mut combined = bytesMake()\n let rooted = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n let mut included = usize.ZERO\n start = usize.ONE\n index = usize.ONE\n while index <= baseBytes.length {\n let mut boundary = false\n if index == baseBytes.length {\n boundary = true\n } else {\n if byte(baseBytes[index]) == 47 { boundary = true }\n }\n if boundary {\n if included < keptBase {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeBaseComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeBaseComponent, baseBytes, start, index)\n included = included + usize.ONE\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n\n start = usize.ZERO\n index = usize.ZERO\n while index <= relative.length {\n let mut boundary = false\n if index == relative.length {\n boundary = true\n } else {\n if byte(relative[index]) == 47 { boundary = true }\n }\n if boundary {\n if start < index {\n if isDot(relative, start, index) == false {\n if isDotDot(relative, start, index) == false {\n if survivingRelative(relative, index + usize.ONE) {\n if usize.ZERO < included {\n let slash = run bytesAppend(&mut combined, stringUtf8Bytes("/"))\n }\n let beforeRelativeComponent = Intrinsic.replace(combined, bytesMake())\n combined = run appendRange(move beforeRelativeComponent, relative, start, index)\n included = included + usize.ONE\n }\n }\n }\n }\n start = index + usize.ONE\n }\n index = index + usize.ONE\n }\n return run finishPath(move combined)\n}\n\n/// Allocates an independently owned parent path, or [`None`] when `self` is root.\n///\n/// # Details\n///\n/// The result does not borrow `self`. A direct child of root has root as its parent.\npub effect fn parent(\n self: &Path\n) -> Option ! OutOfMemoryError ? &mut Allocator {\n if isRoot(self) { return none() }\n let values = pathBytes(self)\n let nameStart = finalNameStart(values)\n let mut end = usize.ONE\n if nameStart != usize.ONE { end = nameStart - usize.ONE }\n let copied = run appendRange(bytesMake(), values, usize.ZERO, end)\n let owned = run finishPath(move copied)\n return some(move owned)\n}\n\n/// Portable mutable service for normalized paths and whole-file operations.\n///\n/// # Details\n///\n/// Application code supplies one provider lexically with `Effect.provideMut`; tests can implement\n/// this service in memory, while native applications can use `silk.os_filesystem`. The service owns\n/// platform policy, but every implementation must preserve the portable error categories,\n/// create-or-truncate writes, and deterministic listing order described here.\n///\n/// # Examples\n/// ## Write a file after creating its parents\n/// ```silk\n/// import silk.allocator { Allocator }\n///\n/// import silk.filesystem as FileSystem\n///\n/// import silk.usize as usize\n///\n/// pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize\n/// ! FileSystem.FileError | Allocator.OutOfMemoryError\n/// ? &mut FileSystem.FileSystem | &mut Allocator {\n/// let written = run FileSystem.writeFileWithParents(path, contents)\n/// return contents.length\n/// }\n/// ```\npub service FileSystem {\n /// Reads one complete regular file into independently owned bytes.\n ///\n /// # Details\n ///\n /// Reading a directory fails with `WrongType`. Allocation of the returned [`Bytes`] may fail\n /// independently of the provider read.\n effect fn readFile(\n path: &Path\n ) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Writes one complete byte view with create-or-truncate semantics.\n ///\n /// # Details\n ///\n /// A missing file is created; an existing regular file is replaced by exactly `bytes`. The call\n /// does not create missing parent directories—use [`writeFileWithParents`] for that workflow.\n effect fn writeFile(path: &Path, bytes: &[u8]) -> () ! FileError ? &mut FileSystem\n /// Returns [`FileInfo`] or [`DirectoryInfo`] for the path without opening file contents.\n ///\n /// # Details\n ///\n /// Missing paths fail with `NotFound`; providers use `WrongType` only when an operation requires a\n /// particular kind, not for this discriminating query.\n effect fn stat(path: &Path) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem\n /// Returns immediate owned children in deterministic complete-path byte order.\n ///\n /// # Details\n ///\n /// The result is not recursive. Each `DirectoryEntry.path` is independently owned and may be\n /// retained after the listing vector is released.\n effect fn listDirectory(\n path: &Path\n ) -> Vector ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n /// Creates exactly one missing directory whose parent already exists.\n ///\n /// # Details\n ///\n /// Existing paths fail with `AlreadyExists`; use [`createDirectoriesRecursively`] to ensure every\n /// missing component.\n effect fn createDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one regular file and fails with `WrongType` for a directory.\n effect fn removeFile(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Removes exactly one empty directory.\n ///\n /// # Details\n ///\n /// A nonempty directory fails with `NotEmpty`; use [`removeDirectoryRecursively`] only when all\n /// descendants are intentionally in scope for removal.\n effect fn removeDirectory(path: &Path) -> () ! FileError ? &mut FileSystem\n /// Creates one directory under an existing parent under a name no other caller holds.\n ///\n /// # Details\n ///\n /// The provider chooses the name\'s unique part and returns the complete Path, because only the\n /// provider can create and claim a name in one step. A caller that supplied the name would have\n /// to check-then-create, and the gap between those two is exactly the race this avoids.\n /// `prefix` is a byte prefix for the provider-chosen child name, not a complete path. The returned\n /// directory already exists and is an immediate child of `parent`.\n effect fn createTemporaryDirectory(\n parent: &Path,\n prefix: &[u8]\n ) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator\n}\n\n/// A directory a caller owns outright, together with everything written inside it.\n///\n/// # Details\n///\n/// Ownership is affine: `TemporaryDirectory` holds an owned `Path`, so exactly one binding holds\n/// it and the compiler rejects a second use of a moved one. Ownership is not, however, a `Drop`\n/// hook. Removing a directory is a fallible operation that requires the `FileSystem` capability,\n/// and a `Drop` hook may carry neither a failure row nor a requirement row, so a hook here could\n/// only be written by inventing an infallible intrinsic over a fallible syscall. Release is\n/// therefore explicit and honest about both rows — see `release`.\n///\n/// Scope ownership comes from composition rather than from a hook: `Effect.ensuring(release)`\n/// runs the release whatever the protected Effect\'s outcome. Because `ensuring` types its\n/// finalizer `! never`, that composition has to say what a failed removal means; `releaseIgnored`\n/// is the stdlib\'s answer and names the loss at the call site.\npub struct TemporaryDirectory {\n /// The complete owned path callers use while the scope remains live.\n pub path: Path\n}\n\n/// Creates an explicitly owned temporary directory under `parent` with a name beginning in `prefix`.\n///\n/// # Details\n///\n/// The result is owned. Nothing removes it until a caller runs [`release`] or [`releaseIgnored`].\n/// The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in\n/// one operation.\npub effect fn temporaryDirectory(\n parent: &Path,\n prefix: string\n) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let created = run FileSystem.createTemporaryDirectory(parent, stringUtf8Bytes(prefix))\n return TemporaryDirectory { path: move created }\n}\n\n/// Consumes one TemporaryDirectory and removes it together with everything inside it.\n///\n/// # Details\n///\n/// Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the\n/// tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a\n/// failed cleanup uses this operation and handles the failure. The owner is consumed even when\n/// removal fails, so copy any diagnostic path information needed before calling.\npub effect fn release(\n self: TemporaryDirectory\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let owned = move self\n let removed = run removeDirectoryRecursively(&owned.path)\n drop owned\n return ()\n}\n\neffect fn discardReleaseFailure(error: FileError | OutOfMemoryError) -> () { return () }\n\n/// Consumes one TemporaryDirectory, removes it, and discards a failed removal.\n///\n/// # Details\n///\n/// This exists because `Effect.ensuring` types its finalizer `! never`, so a fallible release has\n/// to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a\n/// caller reading `releaseIgnored` at the call site can see that a failed removal is being\n/// dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —\n/// a directory the host will reap — and what is kept is the protected Effect\'s own outcome, which\n/// is the answer the program was computing.\n///\n/// A caller who needs the failure uses `release` instead and does not compose it with `ensuring`.\n///\n/// The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer\n/// starts. Derive the required paths before you give the owner to the finalizer.\npub effect fn releaseIgnored(\n self: TemporaryDirectory\n) -> () ? &mut FileSystem | &mut Allocator {\n return run Effect.catchAll(release(move self), discardReleaseFailure)\n}\n\n/// Copies one recorded Path out of the walk\'s own record.\n///\n/// The walk appends to the same record it is reading, so it reads through a copy rather than\n/// through a borrow that the next append would invalidate.\neffect fn recordedCopy(\n recorded: &Vector,\n index: usize\n) -> Path ! FileError | OutOfMemoryError ? &mut Allocator {\n return match &vectorAsSlice(recorded)[index] {\n Path { bytes, nameBytes } => run fromBytes(bytesAsSlice(&bytes))\n }\n}\n\n/// Removes a directory, every descendant file, and every descendant directory.\n///\n/// # Details\n///\n/// Two passes, because the portable primitive removes exactly one *empty* directory. The first\n/// pass walks the tree front to back, unlinking every file it meets and recording every directory\n/// it meets; the second removes the recorded directories back to front. That order is\n/// child-before-parent for free: a directory is always recorded before the children found inside\n/// it, so reversing the record reverses the containment. Neither pass recurses, so depth costs\n/// vector capacity rather than stack.\n///\n/// This operation is destructive and not transactional. If a provider or allocation failure occurs,\n/// removals already completed remain completed and the remaining tree is left in place.\npub effect fn removeDirectoryRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let mut recorded = vectorMake()\n let seed = run fromBytes(rawBytes(path))\n let noted = run vectorAppend(&mut recorded, move seed)\n let mut index = usize.ZERO\n while index < vectorLength(&recorded) {\n let current = run recordedCopy(&recorded, index)\n let entries = run FileSystem.listDirectory(¤t)\n let listed = vectorAsSlice(&entries)\n let mut cursor = usize.ZERO\n while cursor < listed.length {\n let childKind = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } => borrowedKindCode(&childEntryKind)\n }\n if childKind == 0 {\n let unlinked = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run FileSystem.removeFile(&childPath)\n }\n } else {\n let toRemove = match &listed[cursor] {\n DirectoryEntry { path: childPath, kind: childEntryKind } =>\n run fromBytes(rawBytes(&childPath))\n }\n let notedChild = run vectorAppend(&mut recorded, move toRemove)\n }\n cursor = cursor + usize.ONE\n }\n index = index + usize.ONE\n }\n while usize.ZERO < vectorLength(&recorded) {\n let taken = vectorPop(&mut recorded)\n let emptied = match move taken {\n Option.Some { value: selected } => move selected\n Option.None => run fromBytes(rawBytes(path))\n }\n let removed = run FileSystem.removeDirectory(&emptied)\n }\n return ()\n}\n\nstruct DirectoryPresent {}\nstruct DirectoryMissing {}\nstruct DirectoryWrongType {}\nstruct DirectoryStatFailure { error: FileError }\n\nfn classifyStatFailure(\n failure: FileError\n) -> DirectoryMissing | DirectoryStatFailure {\n if failure.reason.code == 0 { return DirectoryMissing {} }\n return DirectoryStatFailure { error: move failure }\n}\n\nfn classifyDirectory(\n outcome: Result\n) -> DirectoryPresent | DirectoryMissing | DirectoryWrongType | DirectoryStatFailure {\n return match move outcome {\n Result.Success { value: info } => match move info {\n DirectoryInfo {} => DirectoryPresent {}\n FileInfo { byteLength } => DirectoryWrongType {}\n }\n Result.Failure { error: failure } => classifyStatFailure(move failure)\n }\n}\n\nfn retainStatInfo(info: FileInfo | DirectoryInfo) -> FileInfo | DirectoryInfo {\n return move info\n}\n\neffect fn statForResult(\n path: &Path\n) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem {\n let info = run FileSystem.stat(path)\n return retainStatInfo(move info)\n}\n\n/// Ensures that `path` and every missing ancestor exist as directories.\n///\n/// # Details\n///\n/// Existing directories are kept. An existing regular file at any component fails with\n/// `WrongType`; failures other than `NotFound` propagate. This is ordinary stat-then-create\n/// composition, so concurrent namespace changes may still race according to provider policy.\npub effect fn createDirectoriesRecursively(\n path: &Path\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n let values = pathBytes(path)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Effect.result(statForResult(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return ()\n}\n\n/// Ensures every parent directory exists, then writes the complete byte view to `path`.\n///\n/// # Details\n///\n/// The final write uses `FileSystem.writeFile` create-or-truncate semantics. Passing root delegates\n/// directly to the provider and normally fails with `WrongType`. Directory creation and writing are\n/// not transactional, so a later failure may leave newly created parents behind.\npub effect fn writeFileWithParents(\n path: &Path,\n bytes: &[u8]\n) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut Allocator {\n if isRoot(path) { return run FileSystem.writeFile(path, bytes) }\n let pathValues = pathBytes(path)\n let nameStart = finalNameStart(pathValues)\n let mut parentEnd = usize.ONE\n if nameStart != usize.ONE { parentEnd = nameStart - usize.ONE }\n let parentBytes = run appendRange(bytesMake(), pathValues, usize.ZERO, parentEnd)\n let ownedParent = run finishPath(move parentBytes)\n let values = pathBytes(&ownedParent)\n let mut index = usize.ONE\n while index <= values.length {\n let mut boundary = false\n if index == values.length {\n boundary = true\n } else {\n if byte(values[index]) == 47 { boundary = true }\n }\n if boundary {\n let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index)\n let prefix = run finishPath(move prefixBytes)\n let completed = run Effect.result(statForResult(&prefix))\n let decision = classifyDirectory(move completed)\n let ensured = match move decision {\n DirectoryPresent {} => ()\n DirectoryMissing {} => run FileSystem.createDirectory(&prefix)\n DirectoryWrongType {} => run raise(error(statOperation(), wrongType()))\n DirectoryStatFailure { error: failure } => run raise(move failure)\n }\n }\n index = index + usize.ONE\n }\n return run FileSystem.writeFile(path, bytes)\n}\n\neffect fn existsFailure(failure: FileError) -> bool ! FileError {\n if failure.reason.code == 0 { return false }\n return run raise(move failure)\n}\n\n/// Returns whether a file or directory exists at `path`.\n///\n/// # Details\n///\n/// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider\n/// failure propagate so callers cannot mistake an inaccessible path for an absent one.\npub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem {\n let completed = run Effect.result(statForResult(path))\n return match move completed {\n Result.Success { value: info } => true\n Result.Failure { error: failure } => run existsFailure(move failure)\n }\n}\n', }, { module: 'silk/format', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index a23a5b953..5e6fedaa2 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '1e87e158465ff7f31e96dc639b2f31299bc88a7fed01e85fb52494c11050afb3' +export const compilerDigest = '60f4e65c61dec49409c5d1cbdb5c56bc7cc43e2b488fe56de0ce84de9a9962a3' diff --git a/packages/compiler/stdlib/silk/filesystem.silk b/packages/compiler/stdlib/silk/filesystem.silk index 3795c17d8..73f28e4da 100644 --- a/packages/compiler/stdlib/silk/filesystem.silk +++ b/packages/compiler/stdlib/silk/filesystem.silk @@ -912,6 +912,17 @@ fn classifyDirectory( } } +fn retainStatInfo(info: FileInfo | DirectoryInfo) -> FileInfo | DirectoryInfo { + return move info +} + +effect fn statForResult( + path: &Path +) -> FileInfo | DirectoryInfo ! FileError ? &mut FileSystem { + let info = run FileSystem.stat(path) + return retainStatInfo(move info) +} + /// Ensures that `path` and every missing ancestor exist as directories. /// /// # Details @@ -934,7 +945,7 @@ pub effect fn createDirectoriesRecursively( if boundary { let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index) let prefix = run finishPath(move prefixBytes) - let completed = run Effect.result(FileSystem.stat(&prefix)) + let completed = run Effect.result(statForResult(&prefix)) let decision = classifyDirectory(move completed) let ensured = match move decision { DirectoryPresent {} => () @@ -978,7 +989,7 @@ pub effect fn writeFileWithParents( if boundary { let prefixBytes = run appendRange(bytesMake(), values, usize.ZERO, index) let prefix = run finishPath(move prefixBytes) - let completed = run Effect.result(FileSystem.stat(&prefix)) + let completed = run Effect.result(statForResult(&prefix)) let decision = classifyDirectory(move completed) let ensured = match move decision { DirectoryPresent {} => () @@ -1004,7 +1015,7 @@ effect fn existsFailure(failure: FileError) -> bool ! FileError { /// Only the portable `NotFound` reason becomes `false`. Permission, I/O, and every other provider /// failure propagate so callers cannot mistake an inaccessible path for an absent one. pub effect fn exists(path: &Path) -> bool ! FileError ? &mut FileSystem { - let completed = run Effect.result(FileSystem.stat(path)) + let completed = run Effect.result(statForResult(path)) return match move completed { Result.Success { value: info } => true Result.Failure { error: failure } => run existsFailure(move failure) diff --git a/packages/compiler/test/ExternalWakeParking.test.ts b/packages/compiler/test/ExternalWakeParking.test.ts index 15b6c4272..ce9f3050a 100644 --- a/packages/compiler/test/ExternalWakeParking.test.ts +++ b/packages/compiler/test/ExternalWakeParking.test.ts @@ -926,7 +926,7 @@ struct Holder { wake: Intrinsic.Wake } fn intrinsic(value: Intrinsic.Wake) -> () { drop value return () } fn ordinary(value: Wake) -> () { drop value return () } fn aggregate(value: Holder) -> () { drop value return () } -fn union(value: Intrinsic.Wake | Empty) -> () { drop value return () } +fn unionValue(value: Intrinsic.Wake | Empty) -> () { drop value return () } fn array(value: [Intrinsic.Wake; 1]) -> () { drop value return () } fn shared(value: Shared.Shared) -> () { drop value return () } pub fn main() -> i32 { return 42 }`), @@ -956,7 +956,7 @@ pub fn main() -> i32 { return 42 }`), ordinary === undefined ? 'Missing' : ExecutionAffinity.ofType(snapshot.index, ordinary)._tag, 'Unrestricted', ) - for (const name of ['aggregate', 'union', 'array', 'shared']) { + for (const name of ['aggregate', 'unionValue', 'array', 'shared']) { const type = parameter(name) assert.strictEqual( type === undefined ? 'Missing' : ExecutionAffinity.ofType(snapshot.index, type)._tag, diff --git a/packages/compiler/test/HashedCollectionPrivilege.test.ts b/packages/compiler/test/HashedCollectionPrivilege.test.ts index 565045da1..b0c569e19 100644 --- a/packages/compiler/test/HashedCollectionPrivilege.test.ts +++ b/packages/compiler/test/HashedCollectionPrivilege.test.ts @@ -35,7 +35,7 @@ import silk.i32 as i32 import silk.hash as Hash import silk.hash { HashKey, HashSeed, Word } import silk.hash_map { HashMap, contains, get, insert, length, make, remove } -import silk.option { Option } +import silk.option { Option, unwrapOr } effect fn build() -> i32 ! OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() @@ -48,10 +48,10 @@ effect fn build() -> i32 ! OutOfMemoryError { key = key + 1 } let taken = remove(&mut map, Hash.word(3)) - let removed = Option.unwrapOr(move taken, -1) + let removed = unwrapOr(move taken, -1) if removed != 3 { return 1 } if !contains(&map, Hash.word(4)) { return 2 } - let held = Option.unwrapOr(get(&map, Hash.word(4)), -1) + let held = unwrapOr(get(&map, Hash.word(4)), -1) if held != 4 { return 3 } return 42 } diff --git a/packages/compiler/test/HashedCollections.test.ts b/packages/compiler/test/HashedCollections.test.ts index 8df410b0e..824be9015 100644 --- a/packages/compiler/test/HashedCollections.test.ts +++ b/packages/compiler/test/HashedCollections.test.ts @@ -75,7 +75,7 @@ import silk.hash_map { valueAt, withMut } -import silk.option { Option } +import silk.option { Option, unwrapOr } import silk.u64 as u64 import silk.usize as usize` @@ -113,13 +113,13 @@ it.effect('inserts, looks up, and removes on both engines', () => if !contains(&map, Hash.word(7)) { return 2 } if contains(&map, Hash.word(11)) { return 3 } let taken = remove(&mut map, Hash.word(7)) - let removed = Option.unwrapOr(move taken, 0) + let removed = unwrapOr(move taken, 0) if length(&map) != 1 { return 4 } if contains(&map, Hash.word(7)) { return 5 } let missing = remove(&mut map, Hash.word(7)) - let absent = Option.unwrapOr(move missing, 0) + let absent = unwrapOr(move missing, 0) if absent != 0 { return 6 } - let held = Option.unwrapOr(get(&map, Hash.word(9)), 0) + let held = unwrapOr(get(&map, Hash.word(9)), 0) return removed + held`, ), ) @@ -158,7 +158,7 @@ fn mustNotRun(value: &mut Counter) -> () { if length(&map) != 1 { return 6 } if bucketCount(&map) != 8 { return 7 } let fallback = Counter { value: 0, calls: 0 } - let held = Option.unwrapOr(get(&map, Hash.word(7)), move fallback) + let held = unwrapOr(get(&map, Hash.word(7)), move fallback) if held.calls != 1 { return 8 } return held.value`, ), @@ -181,10 +181,10 @@ it.effect('reaches one entry from two equivalent keys, and replaces rather than // A second key equivalent to the first finds the entry the first placed. if !contains(&map, Hash.word(3)) { return 1 } let second = run insert(&mut map, Hash.word(3), 31) |> Effect.provideMut(&mut allocator) - let replaced = Option.unwrapOr(move second, 0) + let replaced = unwrapOr(move second, 0) if length(&map) != 1 { return 2 } if replaced != 11 { return 3 } - let held = Option.unwrapOr(get(&map, Hash.word(3)), 0) + let held = unwrapOr(get(&map, Hash.word(3)), 0) if held != 31 { return 4 } return replaced + held`, ), @@ -215,7 +215,7 @@ it.effect('keeps every entry across the growth that rehomes them', () => let mut probe = 0 let mut total = 0 while probe < 40 { - let found = Option.unwrapOr(get(&map, Hash.word(i32.toU64(probe))), -1) + let found = unwrapOr(get(&map, Hash.word(i32.toU64(probe))), -1) if found != probe * 3 { return 3 } total = total + found probe = probe + 1 @@ -246,7 +246,7 @@ import silk.hash { Word } import silk.hash_map { HashMap } import silk.i32 as i32 import silk.layout { Layout } -import silk.option { Option } +import silk.option { Option, unwrapOr } import silk.usize as usize ${mapImports} @@ -294,7 +294,7 @@ effect fn build() -> i32 ! OutOfMemoryError { let mut probe = 0 let mut total = 0 while probe < 6 { - let found = Option.unwrapOr(get(&map, Hash.word(i32.toU64(probe))), -1) + let found = unwrapOr(get(&map, Hash.word(i32.toU64(probe))), -1) if found != probe + 100 { return 7 } total = total + found probe = probe + 1 @@ -335,7 +335,7 @@ it.effect('keeps probing through the marks a removal leaves behind', () => // Every key of this round must be present while the previous rounds' marks are still there. let mut check = 0 while check < 6 { - let found = Option.unwrapOr(get(&map, Hash.word(i32.toU64(round * 6 + check))), -1) + let found = unwrapOr(get(&map, Hash.word(i32.toU64(round * 6 + check))), -1) if found != round * 6 + check { return 1 } check = check + 1 } @@ -344,7 +344,7 @@ it.effect('keeps probing through the marks a removal leaves behind', () => let mut gone = 0 while gone < 6 { let taken = remove(&mut map, Hash.word(i32.toU64(round * 6 + gone))) - let removed = Option.unwrapOr(move taken, -1) + let removed = unwrapOr(move taken, -1) if removed != round * 6 + gone { return 3 } gone = gone + 1 } diff --git a/packages/compiler/test/LexerPressure.test.ts b/packages/compiler/test/LexerPressure.test.ts index 1b0c69a9f..7200b74c5 100644 --- a/packages/compiler/test/LexerPressure.test.ts +++ b/packages/compiler/test/LexerPressure.test.ts @@ -315,7 +315,7 @@ const corpus = [ Object.freeze({ id: 'keywords', input: - 'pub struct enum service interface role effect fn run fail drop unsafe impl for return import as let mut once move match if else while break continue true false const name _x2', + 'pub struct enum union service interface role effect fn run fail drop unsafe impl for return import as let mut once move match if else while break continue true false const name _x2', }), Object.freeze({ id: 'numbers', input: '0 42 1.25 2e3 3E+4 4e- 5..6' }), Object.freeze({ diff --git a/packages/compiler/test/StoredCallableDiagnostic.test.ts b/packages/compiler/test/StoredCallableDiagnostic.test.ts index c299f1cc5..a52443bee 100644 --- a/packages/compiler/test/StoredCallableDiagnostic.test.ts +++ b/packages/compiler/test/StoredCallableDiagnostic.test.ts @@ -208,14 +208,14 @@ pub fn main() -> i32 { it.effect('points a stdlib construction reached through inference at the user call', () => Effect.gen(function* () { - // `Option.some(i32.add(1))` specializes `Option.Some` with a callable argument. The construction + // `some(i32.add(1))` specializes `Option.Some` with a callable argument. The construction // that cannot receive a layout lives inside silk/option, but the callable was written at the // user's call, so the primary span is the user source and the stdlib construction is related // provenance. const source = `import silk.i32 as i32 -import silk.option { Option } +import silk.option { some } pub fn main() -> i32 { - let optional = Option.some(i32.add(1)) + let optional = some(i32.add(1)) return 42 }` const snapshot = yield* analyzed('stored-callable/stdlib-inference', source) diff --git a/packages/compiler/test/support/corpus.ts b/packages/compiler/test/support/corpus.ts index 54b40762c..499aa106b 100644 --- a/packages/compiler/test/support/corpus.ts +++ b/packages/compiler/test/support/corpus.ts @@ -1460,13 +1460,13 @@ pub fn main() -> i32 { { name: 'arith-convergence-checked-remainder-min-none', source: `import silk.i32 as i32 -import silk.option { Option } +import silk.option { Option, unwrapOr } pub fn main() -> i32 { let minimum = i32.subtract(-2147483647, 1) - if Option.unwrapOr(i32.checkedRemainder(minimum, -1), 42) != 42 { return 1 } - if Option.unwrapOr(i32.checkedRemainder(7, -1), -1) != 0 { return 2 } - if Option.unwrapOr(i32.checkedRemainder(minimum, 2), -1) != 0 { return 3 } - if Option.unwrapOr(i32.checkedRemainder(7, 0), 42) != 42 { return 4 } + if unwrapOr(i32.checkedRemainder(minimum, -1), 42) != 42 { return 1 } + if unwrapOr(i32.checkedRemainder(7, -1), -1) != 0 { return 2 } + if unwrapOr(i32.checkedRemainder(minimum, 2), -1) != 0 { return 3 } + if unwrapOr(i32.checkedRemainder(7, 0), 42) != 42 { return 4 } return 42 }`, expected: { _tag: 'Completes', result: 42 }, @@ -1783,7 +1783,7 @@ import silk.string { scalarValue, nextCursor } -import silk.option { Option } +import silk.option { Option, unwrapOr } import silk.char { toU32 as charToU32 } fn scalarSum(value: string, cursor: ScalarCursor) -> u32 { @@ -1956,7 +1956,7 @@ effect fn build() -> i32 ! OutOfMemoryError { let mut probe = 0 let mut total = 0 while probe < 40 { - let found = Option.unwrapOr(get(&map, Hash.word(i32.toU64(probe))), -1) + let found = unwrapOr(get(&map, Hash.word(i32.toU64(probe))), -1) if found != probe * 3 { return 3 } total = total + found probe = probe + 1 From a9a0eb44c6df776d1983a8cdfd8d991de633596b Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 19:46:29 -0300 Subject: [PATCH 25/42] fix(compiler): preserve catch results across suspension --- packages/compiler/src/LowerExpression.ts | 13 +++++++- .../compiler/src/NativeEffectOperation.ts | 30 +++++++++---------- packages/compiler/src/NativeFunction.ts | 6 ++++ packages/compiler/src/SuspensionOwnership.ts | 4 +++ .../src/ToolchainIntegrity.generated.ts | 2 +- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 41e815869..07e395308 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -1432,7 +1432,18 @@ export function lowerExpressionInner( ) : member } - const members = Object.freeze(expression.members.map(specializeMember)) + const specializedMembers = expression.members.map(specializeMember) + const members = + scrutineeType._tag === 'Enum' + ? Object.freeze( + specializedMembers.filter( + (member, ordinal) => + specializedMembers.findIndex((candidate) => + Match.identityEquals(candidate, member), + ) === ordinal, + ), + ) + : Layout.coverageMembers(scrutineeShape) const specializedCoverage = Match.cover( members, expression.arms.map((arm) => diff --git a/packages/compiler/src/NativeEffectOperation.ts b/packages/compiler/src/NativeEffectOperation.ts index 0dbd5b03c..79279e99a 100644 --- a/packages/compiler/src/NativeEffectOperation.ts +++ b/packages/compiler/src/NativeEffectOperation.ts @@ -881,15 +881,14 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op } return Object.freeze(coerced) }) - nativeStorage.locals.set( - operation.successValue.ordinal, - yield* coerce( - Object.freeze(outcomeValues.slice(1, 1 + successLaneCount)), - Object.freeze(outcomeLanes.slice(1, 1 + successLaneCount)), - operation.successShape.lanes, - `effect_result${operation.destination.ordinal}_success`, - ), + const successValues = yield* coerce( + Object.freeze(outcomeValues.slice(1, 1 + successLaneCount)), + Object.freeze(outcomeLanes.slice(1, 1 + successLaneCount)), + operation.successShape.lanes, + `effect_result${operation.destination.ordinal}_success`, ) + nativeStorage.locals.set(operation.successValue.ordinal, successValues) + yield* NativeStorage.storeMutable(nativeStorage, operation.successValue, successValues) const failureValues: Array = [] const failureLanes: Array = [] if (SilkType.isUnion(operation.failureValueType)) { @@ -909,15 +908,14 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op } failureValues.push(...outcomeValues.slice(1)) failureLanes.push(...outcomeLanes.slice(1)) - nativeStorage.locals.set( - operation.failureValue.ordinal, - yield* coerce( - Object.freeze(failureValues), - Object.freeze(failureLanes), - operation.failureValueShape.lanes, - `effect_result${operation.destination.ordinal}_failure`, - ), + const coercedFailureValues = yield* coerce( + Object.freeze(failureValues), + Object.freeze(failureLanes), + operation.failureValueShape.lanes, + `effect_result${operation.destination.ordinal}_failure`, ) + nativeStorage.locals.set(operation.failureValue.ordinal, coercedFailureValues) + yield* NativeStorage.storeMutable(nativeStorage, operation.failureValue, coercedFailureValues) break } case 'CloseEffectEntry': { diff --git a/packages/compiler/src/NativeFunction.ts b/packages/compiler/src/NativeFunction.ts index b13eb6362..44d4a5c61 100644 --- a/packages/compiler/src/NativeFunction.ts +++ b/packages/compiler/src/NativeFunction.ts @@ -67,6 +67,12 @@ export const discoverRoots = ( const runtimeContinuationDestinations = blocks.flatMap((block) => block.operations.flatMap((operation) => { if (!opensRuntimeContinuation(operation) || operation._tag === 'Binary') return [] + if (operation._tag === 'CatchEffect') + return [ + operation.destination.ordinal, + operation.successValue.ordinal, + operation.failureValue.ordinal, + ] const destination = destinationOf(operation) return destination === undefined ? [] : [destination.ordinal] }), diff --git a/packages/compiler/src/SuspensionOwnership.ts b/packages/compiler/src/SuspensionOwnership.ts index af01ec634..1f2333701 100644 --- a/packages/compiler/src/SuspensionOwnership.ts +++ b/packages/compiler/src/SuspensionOwnership.ts @@ -166,6 +166,10 @@ const operationDefinitions = (operation: Mir.Operation): ReadonlySet => nested._tag === 'CloseEffectEntry' ) definitions.add(nested.outcome.ordinal) + if (nested._tag === 'CatchEffect') { + definitions.add(nested.successValue.ordinal) + definitions.add(nested.failureValue.ordinal) + } if (nested._tag === 'Match') for (const arm of nested.arms) for (const binding of arm.bindings) definitions.add(binding.destination.ordinal) diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 5e6fedaa2..5d605f6fc 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '60f4e65c61dec49409c5d1cbdb5c56bc7cc43e2b488fe56de0ce84de9a9962a3' +export const compilerDigest = '1d2c7c84ff96aa3710f4da2bf44a18bc986f2cbeb94233bf4f0b198df55f1529' From c682c77a4001a775daf84559d1ef211d4a30e525 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 19:53:46 -0300 Subject: [PATCH 26/42] fix(compiler): plan erased catch specializations --- packages/compiler/src/ProvisionalMir.ts | 69 ++++++++++++++++++- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/test/ProvisionalMir.test.ts | 11 ++- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/ProvisionalMir.ts b/packages/compiler/src/ProvisionalMir.ts index cc95a0048..1191b43c5 100644 --- a/packages/compiler/src/ProvisionalMir.ts +++ b/packages/compiler/src/ProvisionalMir.ts @@ -737,6 +737,19 @@ const effectCatchOf = ( return expression._tag === 'EffectCatch' ? expression : undefined } +const runSpanOfCatch = ( + statements: ReadonlyArray, + target: Extract, + context: BuildContext, +): SourceSpan.SourceSpan => + statements + .flatMap(Hir.statementExpressions) + .flatMap(Hir.expressionTree) + .find( + (candidate) => + candidate._tag === 'Run' && effectCatchOf(candidate.subject, context) === target, + )?.span ?? target.span + const catchHandlerRunner = ( expression: Extract, context: BuildContext, @@ -789,6 +802,7 @@ const controlsOfCatch = ( execution: ExecutionKey, context: BuildContext, ordinalOffset = 0, + runSpan = expression.span, ): ReadonlyArray => { const regions: Array = [] if (expression.protected._tag === 'Unavailable') return Object.freeze(regions) @@ -797,6 +811,43 @@ const controlsOfCatch = ( if (!Type.isEffect(protectedEffect) || !Type.isEffect(resultEffect)) return Object.freeze([]) const protectedRunner = runnerOf(expression.protected, context) + const selected = Type.substitute(expression.selected, context.instance.substitution) + if (Type.isNever(selected)) { + if (protectedRunner.classification === 'Synchronous') return Object.freeze(regions) + const policy: Extract = Object.freeze({ + _tag: 'Propagate', + outcome: protectedRunner.outcome, + failureMappings: Object.freeze( + Type.failureMembers(protectedRunner.outcome).map((_failure, source) => + Object.freeze({ source: source + 1, target: source + 1 }), + ), + ), + }) + const id = controlId(execution, runSpan, ordinalOffset, 'Invoke') + const complete = controlId(execution, runSpan, ordinalOffset, 'Complete') + return Object.freeze([ + Object.freeze({ + _tag: 'ProvisionalRegion', + id, + outcome: Object.freeze({ + _tag: 'RunSuspendableEffect', + runner: protectedRunner, + completion: policy, + complete, + relay: Object.freeze({ + _tag: 'RelayExistingTransfer', + preserves: ['Child', 'Origin', 'TypedOutcome'] as const, + }), + span: runSpan, + }), + }), + Object.freeze({ + _tag: 'ProvisionalRegion', + id: complete, + outcome: Object.freeze({ _tag: 'Complete', policy }), + }), + ]) + } const protectedPolicy = reifyPolicy(protectedRunner.outcome, context) if (protectedRunner.classification !== 'Synchronous' && protectedPolicy !== undefined) { const id = controlId(execution, expression.span, ordinalOffset, 'Invoke') @@ -893,7 +944,7 @@ const controlsOf = ( ordinal += 1 const caught = effectCatchOf(expression.subject, context) if (caught !== undefined) { - regions.push(...controlsOfCatch(caught, execution, context, idOrdinal)) + regions.push(...controlsOfCatch(caught, execution, context, idOrdinal, expression.span)) ordinal += 1 return } @@ -1086,7 +1137,13 @@ export const build = ( const regions = expression._tag === 'EffectBlock' ? controlsOf(expression.statements, key, runnerClassification, runnerContext) - : controlsOfCatch(expression, key, runnerContext) + : controlsOfCatch( + expression, + key, + runnerContext, + 0, + runSpanOfCatch(instance.function.statements, expression, runnerContext), + ) if (expression._tag === 'EffectBlock') observedProvided.push(...providedRunnersOf(expression.statements, runnerContext)) else @@ -1170,7 +1227,13 @@ export const build = ( const regions = body._tag === 'EffectBlock' ? controlsOf(body.statements, key, runner.classification, context) - : controlsOfCatch(body, key, context) + : controlsOfCatch( + body, + key, + context, + 0, + runSpanOfCatch(owner.function.statements, body, context), + ) if (body._tag === 'EffectBlock') pendingProvided.push(...providedRunnersOf(body.statements, context)) const relaysProvidedRunner = regions.some( diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 5d605f6fc..0c1966b94 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '1d2c7c84ff96aa3710f4da2bf44a18bc986f2cbeb94233bf4f0b198df55f1529' +export const compilerDigest = 'dc293d949feac0dc8f7fb1513cd6d69ee4ff7971ab8cfb3790556da88d13daf9' diff --git a/packages/compiler/test/ProvisionalMir.test.ts b/packages/compiler/test/ProvisionalMir.test.ts index 2602b4fcb..a5e5b5e9f 100644 --- a/packages/compiler/test/ProvisionalMir.test.ts +++ b/packages/compiler/test/ProvisionalMir.test.ts @@ -56,7 +56,7 @@ pub fn main() -> i32 { }), ) -it.effect('retains Reify completion for ordinary source-defined combinators', () => +it.effect('uses ordinary propagation for source-defined combinators', () => Effect.gen(function* () { const self = yield* snapshot(`import silk.effect as Effect effect fn seed(value: i32) -> i32 { @@ -72,12 +72,11 @@ pub fn main() -> i32 { assert.deepEqual(Analysis.diagnostics(self), []) const provisional = available(self) assert.deepEqual(ProvisionalMir.verify(provisional), []) - assert.isTrue( - outcomes(provisional).some( - (control) => control._tag === 'RunSuspendableEffect' && control.completion._tag === 'Reify', - ), - ProvisionalMir.encode(provisional), + const relays = outcomes(provisional).filter( + (control) => control._tag === 'RunSuspendableEffect', ) + assert.isAtLeast(relays.length, 1) + assert.isTrue(relays.every((control) => control.completion._tag === 'Propagate')) }), ) From d6555de38f786a5461fc2bea3a9a9ede3fd3ea44 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 20:23:18 -0300 Subject: [PATCH 27/42] fix(compiler): verify nominal union fallbacks --- packages/compiler/src/MirVerification.ts | 8 +- .../src/ToolchainIntegrity.generated.ts | 2 +- .../test/RecursionStackBoundary.test.ts | 14 +- .../test/SynchronousEffectCost.test.ts | 2 +- .../test/fixtures/synchronous-effect-cost.mjs | 9 +- .../test/goldens/algorithmic.mir.sha256 | 2 +- .../compiler/test/goldens/logging.mir.txt | 1111 ++++++++--------- packages/compiler/test/goldens/match.mir.txt | 4 +- .../compiler/test/goldens/operator.mir.txt | 12 +- packages/compiler/test/support/corpus.ts | 2 +- 10 files changed, 569 insertions(+), 597 deletions(-) diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 5be9c25ef..23f60ebf6 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -4494,7 +4494,7 @@ export const verify = (self: Module): ReadonlyArray => { const localType = fn.localTypes.at(binding.destination.ordinal) const selected = arm.member === undefined - ? undefined + ? fieldPathType(self.layout, semanticType(operation.scrutineeType), binding.path) : coverageFieldPathType(self.layout, arm.member, binding.path) if ( localType === undefined || @@ -4553,7 +4553,11 @@ export const verify = (self: Module): ReadonlyArray => { ? arm.selected.cleanup.every((entry) => { const selected = arm.member === undefined - ? undefined + ? fieldPathType( + self.layout, + semanticType(operation.scrutineeType), + entry.path, + ) : coverageFieldPathType(self.layout, arm.member, entry.path) return ( selected !== undefined && diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 0c1966b94..fdd513a0d 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'dc293d949feac0dc8f7fb1513cd6d69ee4ff7971ab8cfb3790556da88d13daf9' +export const compilerDigest = '089a85776931de08fc3161b1b88eb3d63630470883eaef5b0372f4876550d72c' diff --git a/packages/compiler/test/RecursionStackBoundary.test.ts b/packages/compiler/test/RecursionStackBoundary.test.ts index 5f4700868..0b1a18801 100644 --- a/packages/compiler/test/RecursionStackBoundary.test.ts +++ b/packages/compiler/test/RecursionStackBoundary.test.ts @@ -391,9 +391,9 @@ it.effect('blocks a deep recursive traversal on the evaluator call-depth limit', assert.strictEqual(evaluated.reason._tag, 'EvaluationLimit') if (evaluated.reason._tag !== 'EvaluationLimit') return assert.strictEqual(evaluated.reason.kind, 'CallDepth') - // The blocked frame names the innermost helper reached by the recursive walk, which is the - // provenance a reader needs to recognise the shape rather than guess at an unrelated limit. - assert.strictEqual(evaluated.reason.function.name, 'get') + // The blocked frame names the recursive walk itself after Option became an ordinary nominal + // union and no longer inserts a separate accessor frame. + assert.strictEqual(evaluated.reason.function.name, 'stepDepth') }), ) @@ -514,10 +514,10 @@ it.effect('blocks a deep recursive Drop on the evaluator call-depth limit', () = assert.strictEqual(evaluated.reason._tag, 'EvaluationLimit') if (evaluated.reason._tag !== 'EvaluationLimit') return assert.strictEqual(evaluated.reason.kind, 'CallDepth') - // The frame that ran out belongs to the standard library, not to the program: the recursive - // drop has reached the slot helper used by `Box`, which no call site wrote. - assert.strictEqual(evaluated.reason.function.module, 'silk/slot') - assert.strictEqual(evaluated.reason.function.name, 'dropValue') + // The frame that ran out belongs to the standard library, not to the program: recursive drop + // remains at the owning `Box` actor now that its optional link is an ordinary union. + assert.strictEqual(evaluated.reason.function.module, 'silk/box') + assert.strictEqual(evaluated.reason.function.name, 'drop@impl#0') }), ) diff --git a/packages/compiler/test/SynchronousEffectCost.test.ts b/packages/compiler/test/SynchronousEffectCost.test.ts index 36a793e36..56c2d116e 100644 --- a/packages/compiler/test/SynchronousEffectCost.test.ts +++ b/packages/compiler/test/SynchronousEffectCost.test.ts @@ -189,7 +189,7 @@ it('captures synchronous Effect entry structure', () => { } const runners = report.cases.flatMap((sample) => sample.runners) - assert.strictEqual(runners.length, 22) + assert.strictEqual(runners.length, 16) assert.isTrue(runners.every((runner) => runner.estimatedClonedSize > 0)) assert.isTrue(runners.every((runner) => !runner.prototypeEligible)) assert.deepEqual( diff --git a/packages/compiler/test/fixtures/synchronous-effect-cost.mjs b/packages/compiler/test/fixtures/synchronous-effect-cost.mjs index 5cd1ee626..706246aed 100644 --- a/packages/compiler/test/fixtures/synchronous-effect-cost.mjs +++ b/packages/compiler/test/fixtures/synchronous-effect-cost.mjs @@ -280,11 +280,16 @@ const directStaticCases = new Set([ 'map-both-failure-effect', 'flat-map-effect', 'provide-effect', - 'stored-effect', 'affine-imperative', 'affine-effect', 'trap-effect', ]) +const constructorOnlyCases = new Set(['stored-effect']) +const applicabilityOf = (id) => { + if (directStaticCases.has(id)) return 'DirectStaticRun' + if (constructorOnlyCases.has(id)) return 'ConstructorOnly' + return 'None' +} const clangText = (bitcode, id, arguments_) => { const bitcodePath = join(temporary, `${id}.bc`) @@ -688,7 +693,7 @@ try { const coroutineFrameDescriptors = loweredWasm.functions.filter( (fn) => fn.suspension?.frame !== undefined, ).length - const applicability = directStaticCases.has(sample.id) ? 'DirectStaticRun' : 'None' + const applicability = applicabilityOf(sample.id) cases.push( Object.freeze({ diff --git a/packages/compiler/test/goldens/algorithmic.mir.sha256 b/packages/compiler/test/goldens/algorithmic.mir.sha256 index 3665ba774..70692ff5b 100644 --- a/packages/compiler/test/goldens/algorithmic.mir.sha256 +++ b/packages/compiler/test/goldens/algorithmic.mir.sha256 @@ -1 +1 @@ -acc87d9da6d4401563915b76b95635b4844de72b998ab4db5d684d5f5fd410e8 +d63f4ee53c1fd59fca513525099a182683aaf445f5ff7bb0d889a963e5aeefb6 diff --git a/packages/compiler/test/goldens/logging.mir.txt b/packages/compiler/test/goldens/logging.mir.txt index 209269267..a3a95f160 100644 --- a/packages/compiler/test/goldens/logging.mir.txt +++ b/packages/compiler/test/goldens/logging.mir.txt @@ -11,28 +11,25 @@ normalization accepted kind=FoldedConstructor function=logging/main.main region= normalization accepted kind=FoldedConstructor function=logging/main.main region=r0 local=%2 guards=DirectTarget,SingleRegion,Synchronous [1670, 1706) normalization rejected reason=EffectEscapes function=logging/main.main region=r0 local=%0 [1687, 1696) normalization accepted kind=DirectStaticRun function=logging/main.main region=r0 local=%4 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [1666, 1706) -normalization rejected reason=EffectEscapes function=silk/effect.catchAll region=r0 local=%2 [14013, 14267) +normalization rejected reason=EffectEscapes function=silk/effect.catchAll region=r0 local=%2 [13853, 13923) normalization rejected reason=EffectEscapes function=logging/main.program region=r0 local=%0 [252, 1577) normalization rejected reason=CrossRegionUse function=logging/main.recover region=r0 local=%1 [1620, 1633) -normalization rejected reason=EffectEscapes function=silk/effect.result region=r0 local=%1 [7275, 7331) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.logTrace region=r0 local=%1 [5288, 5336) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.logDebug region=r0 local=%1 [5504, 5552) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.log region=r0 local=%1 [4673, 4720) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.logInfo region=r0 local=%1 [5718, 5765) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.logWarning region=r0 local=%1 [5937, 5987) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.logError region=r0 local=%1 [6155, 6203) -normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [23232, 23322) -normalization rejected reason=EffectEscapes function=silk/effect.logAt region=r0 local=%2 [5071, 5120) -normalization rejected reason=EffectEscapes function=silk/logger.record region=r0 local=%3 [7774, 8926) -normalization rejected reason=EffectEscapes function=silk/logger.reject region=r0 local=%1 [3001, 3036) -normalization accepted kind=FoldedConstructor function=silk/effect.catchAll$effect$-1 region=r0 local=%2 guards=DirectTarget,SingleRegion,Synchronous [14037, 14055) -normalization accepted kind=DirectStaticRun function=silk/effect.catchAll$effect$-1 region=r0 local=%4 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [14033, 14055) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.logTrace region=r0 local=%1 [5280, 5328) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.logDebug region=r0 local=%1 [5496, 5544) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.log region=r0 local=%1 [4665, 4712) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.logInfo region=r0 local=%1 [5710, 5757) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.logWarning region=r0 local=%1 [5929, 5979) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.logError region=r0 local=%1 [6147, 6195) +normalization rejected reason=EffectEscapes function=silk/effect.provideMut region=r0 local=%2 [22790, 22880) +normalization rejected reason=EffectEscapes function=silk/effect.logAt region=r0 local=%2 [5063, 5112) +normalization rejected reason=EffectEscapes function=silk/logger.record region=r0 local=%3 [7727, 8879) +normalization rejected reason=EffectEscapes function=silk/logger.reject region=r0 local=%1 [3013, 3048) normalization accepted kind=FoldedConstructor function=logging/main.program$effect$-1 region=r1 local=%4 guards=DirectTarget,SingleRegion,Synchronous [336, 360) normalization accepted kind=FoldedConstructor function=logging/main.program$effect$-1 region=r2 local=%10 guards=DirectTarget,SingleRegion,Synchronous [411, 435) normalization accepted kind=FoldedConstructor function=logging/main.program$effect$-1 region=r3 local=%16 guards=DirectTarget,SingleRegion,Synchronous [485, 503) @@ -47,26 +44,26 @@ normalization accepted kind=DirectStaticRun function=logging/main.program$effect normalization accepted kind=DirectStaticRun function=logging/main.program$effect$-1 region=r5 local=%30 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [616, 681) normalization accepted kind=DirectStaticRun function=logging/main.program$effect$-1 region=r6 local=%36 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [695, 756) normalization accepted kind=DirectStaticRun function=logging/main.program$effect$-1 region=r7 local=%43 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [773, 868) -normalization accepted kind=FoldedConstructor function=silk/logger.record$effect$-1 region=r9 local=%16 guards=DirectTarget,SingleRegion,Synchronous [7963, 7973) -normalization accepted kind=FoldedConstructor function=silk/logger.record$effect$-1 region=r14 local=%26 guards=DirectTarget,SingleRegion,Synchronous [8028, 8038) -normalization accepted kind=FoldedConstructor function=silk/logger.record$effect$-1 region=r19 local=%38 guards=DirectTarget,SingleRegion,Synchronous [8112, 8122) -normalization accepted kind=DirectStaticRun function=silk/logger.record$effect$-1 region=r9 local=%18 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [7959, 7973) -normalization accepted kind=DirectStaticRun function=silk/logger.record$effect$-1 region=r14 local=%28 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [8024, 8038) -normalization accepted kind=DirectStaticRun function=silk/logger.record$effect$-1 region=r19 local=%40 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [8108, 8122) -normalization accepted kind=FoldedConstructor function=silk/effect.logTrace$effect$-1$provided$20 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5303, 5334) -normalization accepted kind=DirectStaticRun function=silk/effect.logTrace$effect$-1$provided$20 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5299, 5334) -normalization accepted kind=FoldedConstructor function=silk/effect.logDebug$effect$-1$provided$21 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5519, 5550) -normalization accepted kind=DirectStaticRun function=silk/effect.logDebug$effect$-1$provided$21 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5515, 5550) -normalization accepted kind=FoldedConstructor function=silk/effect.log$effect$-1$provided$22 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [4688, 4718) -normalization accepted kind=DirectStaticRun function=silk/effect.log$effect$-1$provided$22 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [4684, 4718) -normalization accepted kind=FoldedConstructor function=silk/effect.logInfo$effect$-1$provided$23 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5733, 5763) -normalization accepted kind=DirectStaticRun function=silk/effect.logInfo$effect$-1$provided$23 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5729, 5763) -normalization accepted kind=FoldedConstructor function=silk/effect.logWarning$effect$-1$provided$24 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5952, 5985) -normalization accepted kind=DirectStaticRun function=silk/effect.logWarning$effect$-1$provided$24 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5948, 5985) -normalization accepted kind=FoldedConstructor function=silk/effect.logError$effect$-1$provided$25 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [6170, 6201) -normalization accepted kind=DirectStaticRun function=silk/effect.logError$effect$-1$provided$25 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [6166, 6201) -normalization accepted kind=FoldedConstructor function=silk/effect.logAt$effect$-1$provided$26 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5086, 5118) -normalization rejected reason=AffineCapture function=silk/effect.logAt$effect$-1$provided$26 region=r0 local=%3 [5086, 5118) +normalization accepted kind=FoldedConstructor function=silk/logger.record$effect$-1 region=r9 local=%16 guards=DirectTarget,SingleRegion,Synchronous [7916, 7926) +normalization accepted kind=FoldedConstructor function=silk/logger.record$effect$-1 region=r14 local=%26 guards=DirectTarget,SingleRegion,Synchronous [7981, 7991) +normalization accepted kind=FoldedConstructor function=silk/logger.record$effect$-1 region=r19 local=%38 guards=DirectTarget,SingleRegion,Synchronous [8065, 8075) +normalization accepted kind=DirectStaticRun function=silk/logger.record$effect$-1 region=r9 local=%18 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [7912, 7926) +normalization accepted kind=DirectStaticRun function=silk/logger.record$effect$-1 region=r14 local=%28 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [7977, 7991) +normalization accepted kind=DirectStaticRun function=silk/logger.record$effect$-1 region=r19 local=%40 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [8061, 8075) +normalization accepted kind=FoldedConstructor function=silk/effect.logTrace$effect$-1$provided$19 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5295, 5326) +normalization accepted kind=DirectStaticRun function=silk/effect.logTrace$effect$-1$provided$19 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5291, 5326) +normalization accepted kind=FoldedConstructor function=silk/effect.logDebug$effect$-1$provided$20 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5511, 5542) +normalization accepted kind=DirectStaticRun function=silk/effect.logDebug$effect$-1$provided$20 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5507, 5542) +normalization accepted kind=FoldedConstructor function=silk/effect.log$effect$-1$provided$21 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [4680, 4710) +normalization accepted kind=DirectStaticRun function=silk/effect.log$effect$-1$provided$21 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [4676, 4710) +normalization accepted kind=FoldedConstructor function=silk/effect.logInfo$effect$-1$provided$22 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5725, 5755) +normalization accepted kind=DirectStaticRun function=silk/effect.logInfo$effect$-1$provided$22 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5721, 5755) +normalization accepted kind=FoldedConstructor function=silk/effect.logWarning$effect$-1$provided$23 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5944, 5977) +normalization accepted kind=DirectStaticRun function=silk/effect.logWarning$effect$-1$provided$23 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [5940, 5977) +normalization accepted kind=FoldedConstructor function=silk/effect.logError$effect$-1$provided$24 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [6162, 6193) +normalization accepted kind=DirectStaticRun function=silk/effect.logError$effect$-1$provided$24 region=r0 local=%5 guards=DirectTarget,SingleRegion,SingleUse,Synchronous,CopyOrShared [6158, 6193) +normalization accepted kind=FoldedConstructor function=silk/effect.logAt$effect$-1$provided$25 region=r0 local=%3 guards=DirectTarget,SingleRegion,Synchronous [5078, 5110) +normalization rejected reason=AffineCapture function=silk/effect.logAt$effect$-1$provided$25 region=r0 local=%3 [5078, 5110) target aarch64-apple-darwin kind=Native pointer=8/8 endian=little layout Array size=256 align=4 repr=repeated element=i32 length=64 stride=4 elements i32 count=64 stride=4 @@ -92,12 +89,6 @@ layout silk/logger.InMemoryLogger size=432 align=8 repr=aggregate cleanup-hook=n layout silk/logger.LogError size=4 align=4 repr=aggregate cleanup-hook=none tail-padding=0 field 0 code: i32 offset=0 size=4 align=4 padding=0 layout silk/logger.LogLevel size=1 align=1 repr=scalar-enum silk/logger.LogLevel lane=u8 bits=8 signedness=unsigned members=Trace=0,Debug=1,Info=2,Warning=3,Error=4 -layout silk/result.Failure size=4 align=4 repr=aggregate cleanup-hook=none tail-padding=0 - field 0 error: silk/logger.LogError offset=0 size=4 align=4 padding=0 -layout silk/result.Result size=8 align=4 repr=aggregate cleanup-hook=none tail-padding=0 - field 0 value: silk/result.Failure | silk/result.Success offset=0 size=8 align=4 padding=0 -layout silk/result.Success size=4 align=4 repr=aggregate cleanup-hook=none tail-padding=0 - field 0 value: i32 offset=0 size=4 align=4 padding=0 layout &mut silk/logger.InMemoryLogger size=8 align=8 repr=reference target=silk/logger.InMemoryLogger address=i64@0/8/8 address Address bits=64 offset=0 size=8 align=8 layout &silk/logger.InMemoryLogger size=8 align=8 repr=reference target=silk/logger.InMemoryLogger address=i64@0/8/8 @@ -109,12 +100,10 @@ layout string size=16 align=8 repr=string storage=Utf8:i64@0/8/8 byte-length=usi storage StringUtf8 bits=64 offset=0 size=8 align=8 byte-length usize offset=8 size=8 layout never size=0 align=1 repr=aggregate cleanup-hook=none tail-padding=0 -layout silk/result.Failure | silk/result.Success size=8 align=4 repr=union tag=i32 payload-offset=4 payload-size=4 payload-align=4 tag-padding=0 tail-padding=0 - member 0 silk/result.Failure size=4 align=4 - member 1 silk/result.Success size=4 align=4 effect-environment logging/main.program@effect:declaration:logging/main:program:site:-1 size=0 align=1 fields=none effect-environment logging/main.recover@effect:declaration:logging/main:recover:site:-1 size=0 align=1 fields=none effect-environment silk/effect.catchAll@effect:declaration:silk/effect:catchAll:site:-1 size=0 align=1 fields=parameter0:shared:value@0,parameter1:shared:callable@0 +effect-environment silk/effect.catchAll@effect:declaration:silk/effect:catchAll:site:1073755692 size=0 align=1 fields=binding0:shared:value@0,binding1:shared:callable@0 effect-environment silk/effect.log@effect:declaration:silk/effect:log:site:-1 size=16 align=8 fields=parameter0:copy:value@0 effect-environment silk/effect.logAt@effect:declaration:silk/effect:logAt:site:-1 size=24 align=8 fields=parameter0:copy:value@0,parameter1:copy:value@8 effect-environment silk/effect.logDebug@effect:declaration:silk/effect:logDebug:site:-1 size=16 align=8 fields=parameter0:copy:value@0 @@ -129,7 +118,6 @@ effect-environment silk/effect.provideMut@effect:declaration:silk/effect:provide effect-environment silk/effect.provideMut@effect:declaration:silk/effect:provideMut:site:-1 size=24 align=8 fields=parameter0:shared:value@0,parameter1:take:value@16 effect-environment silk/effect.provideMut@effect:declaration:silk/effect:provideMut:site:-1 size=24 align=8 fields=parameter0:shared:value@0,parameter1:take:value@16 effect-environment silk/effect.provideMut@effect:declaration:silk/effect:provideMut:site:-1 size=24 align=8 fields=parameter0:shared:value@0,parameter1:take:value@16 -effect-environment silk/effect.result@effect:declaration:silk/effect:result:site:-1 size=0 align=1 fields=parameter0:shared:value@0 effect-environment silk/logger.record@effect:declaration:silk/logger:record:site:-1 size=32 align=8 fields=parameter0:take:value@0,parameter1:copy:value@8,parameter2:copy:value@16 effect-environment silk/logger.reject@effect:declaration:silk/logger:reject:site:-1 size=4 align=4 fields=parameter0:copy:value@0 calling Array lanes=64 i32[[0]],i32[[1]],i32[[2]],i32[[3]],i32[[4]],i32[[5]],i32[[6]],i32[[7]],i32[[8]],i32[[9]],i32[[10]],i32[[11]],i32[[12]],i32[[13]],i32[[14]],i32[[15]],i32[[16]],i32[[17]],i32[[18]],i32[[19]],i32[[20]],i32[[21]],i32[[22]],i32[[23]],i32[[24]],i32[[25]],i32[[26]],i32[[27]],i32[[28]],i32[[29]],i32[[30]],i32[[31]],i32[[32]],i32[[33]],i32[[34]],i32[[35]],i32[[36]],i32[[37]],i32[[38]],i32[[39]],i32[[40]],i32[[41]],i32[[42]],i32[[43]],i32[[44]],i32[[45]],i32[[46]],i32[[47]],i32[[48]],i32[[49]],i32[[50]],i32[[51]],i32[[52]],i32[[53]],i32[[54]],i32[[55]],i32[[56]],i32[[57]],i32[[58]],i32[[59]],i32[[60]],i32[[61]],i32[[62]],i32[[63]] @@ -145,26 +133,20 @@ calling Effect lanes=2 i32[tag],i32[payload[0]] calling Effect<()> lanes=1 i32[tag] calling Effect<() ! silk/logger.LogError> lanes=2 i32[tag],i32[payload[0]] calling Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> lanes=2 i32[tag],i32[payload[0]] -calling Effect> lanes=3 i32[tag],i32[payload[0]],i32[payload[1]] calling Effect lanes=2 i32[tag],i32[payload[0]] calling once Effect lanes=2 i32[tag],i32[payload[0]] calling once Effect lanes=2 i32[tag],i32[payload[0]] calling once Effect<() ! silk/logger.LogError> lanes=2 i32[tag],i32[payload[0]] calling once Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> lanes=2 i32[tag],i32[payload[0]] -calling once Effect> lanes=3 i32[tag],i32[payload[0]],i32[payload[1]] calling () lanes=0 -calling silk/logger.InMemoryLogger lanes=93 u8[silk/logger#9.0.[0]],u8[silk/logger#9.0.[1]],u8[silk/logger#9.0.[2]],u8[silk/logger#9.0.[3]],u8[silk/logger#9.0.[4]],u8[silk/logger#9.0.[5]],u8[silk/logger#9.0.[6]],u8[silk/logger#9.0.[7]],usize[silk/logger#9.1.[0]],usize[silk/logger#9.1.[1]],usize[silk/logger#9.1.[2]],usize[silk/logger#9.1.[3]],usize[silk/logger#9.1.[4]],usize[silk/logger#9.1.[5]],usize[silk/logger#9.1.[6]],usize[silk/logger#9.1.[7]],usize[silk/logger#9.2.[0]],usize[silk/logger#9.2.[1]],usize[silk/logger#9.2.[2]],usize[silk/logger#9.2.[3]],usize[silk/logger#9.2.[4]],usize[silk/logger#9.2.[5]],usize[silk/logger#9.2.[6]],usize[silk/logger#9.2.[7]],i32[silk/logger#9.3.[0]],i32[silk/logger#9.3.[1]],i32[silk/logger#9.3.[2]],i32[silk/logger#9.3.[3]],i32[silk/logger#9.3.[4]],i32[silk/logger#9.3.[5]],i32[silk/logger#9.3.[6]],i32[silk/logger#9.3.[7]],i32[silk/logger#9.3.[8]],i32[silk/logger#9.3.[9]],i32[silk/logger#9.3.[10]],i32[silk/logger#9.3.[11]],i32[silk/logger#9.3.[12]],i32[silk/logger#9.3.[13]],i32[silk/logger#9.3.[14]],i32[silk/logger#9.3.[15]],i32[silk/logger#9.3.[16]],i32[silk/logger#9.3.[17]],i32[silk/logger#9.3.[18]],i32[silk/logger#9.3.[19]],i32[silk/logger#9.3.[20]],i32[silk/logger#9.3.[21]],i32[silk/logger#9.3.[22]],i32[silk/logger#9.3.[23]],i32[silk/logger#9.3.[24]],i32[silk/logger#9.3.[25]],i32[silk/logger#9.3.[26]],i32[silk/logger#9.3.[27]],i32[silk/logger#9.3.[28]],i32[silk/logger#9.3.[29]],i32[silk/logger#9.3.[30]],i32[silk/logger#9.3.[31]],i32[silk/logger#9.3.[32]],i32[silk/logger#9.3.[33]],i32[silk/logger#9.3.[34]],i32[silk/logger#9.3.[35]],i32[silk/logger#9.3.[36]],i32[silk/logger#9.3.[37]],i32[silk/logger#9.3.[38]],i32[silk/logger#9.3.[39]],i32[silk/logger#9.3.[40]],i32[silk/logger#9.3.[41]],i32[silk/logger#9.3.[42]],i32[silk/logger#9.3.[43]],i32[silk/logger#9.3.[44]],i32[silk/logger#9.3.[45]],i32[silk/logger#9.3.[46]],i32[silk/logger#9.3.[47]],i32[silk/logger#9.3.[48]],i32[silk/logger#9.3.[49]],i32[silk/logger#9.3.[50]],i32[silk/logger#9.3.[51]],i32[silk/logger#9.3.[52]],i32[silk/logger#9.3.[53]],i32[silk/logger#9.3.[54]],i32[silk/logger#9.3.[55]],i32[silk/logger#9.3.[56]],i32[silk/logger#9.3.[57]],i32[silk/logger#9.3.[58]],i32[silk/logger#9.3.[59]],i32[silk/logger#9.3.[60]],i32[silk/logger#9.3.[61]],i32[silk/logger#9.3.[62]],i32[silk/logger#9.3.[63]],usize[silk/logger#9.4],usize[silk/logger#9.5],usize[silk/logger#9.6],bool[silk/logger#9.7],usize[silk/logger#9.8] -calling silk/logger.LogError lanes=1 i32[silk/logger#1.0] +calling silk/logger.InMemoryLogger lanes=93 u8[struct:silk/logger:9:0.[0]],u8[struct:silk/logger:9:0.[1]],u8[struct:silk/logger:9:0.[2]],u8[struct:silk/logger:9:0.[3]],u8[struct:silk/logger:9:0.[4]],u8[struct:silk/logger:9:0.[5]],u8[struct:silk/logger:9:0.[6]],u8[struct:silk/logger:9:0.[7]],usize[struct:silk/logger:9:1.[0]],usize[struct:silk/logger:9:1.[1]],usize[struct:silk/logger:9:1.[2]],usize[struct:silk/logger:9:1.[3]],usize[struct:silk/logger:9:1.[4]],usize[struct:silk/logger:9:1.[5]],usize[struct:silk/logger:9:1.[6]],usize[struct:silk/logger:9:1.[7]],usize[struct:silk/logger:9:2.[0]],usize[struct:silk/logger:9:2.[1]],usize[struct:silk/logger:9:2.[2]],usize[struct:silk/logger:9:2.[3]],usize[struct:silk/logger:9:2.[4]],usize[struct:silk/logger:9:2.[5]],usize[struct:silk/logger:9:2.[6]],usize[struct:silk/logger:9:2.[7]],i32[struct:silk/logger:9:3.[0]],i32[struct:silk/logger:9:3.[1]],i32[struct:silk/logger:9:3.[2]],i32[struct:silk/logger:9:3.[3]],i32[struct:silk/logger:9:3.[4]],i32[struct:silk/logger:9:3.[5]],i32[struct:silk/logger:9:3.[6]],i32[struct:silk/logger:9:3.[7]],i32[struct:silk/logger:9:3.[8]],i32[struct:silk/logger:9:3.[9]],i32[struct:silk/logger:9:3.[10]],i32[struct:silk/logger:9:3.[11]],i32[struct:silk/logger:9:3.[12]],i32[struct:silk/logger:9:3.[13]],i32[struct:silk/logger:9:3.[14]],i32[struct:silk/logger:9:3.[15]],i32[struct:silk/logger:9:3.[16]],i32[struct:silk/logger:9:3.[17]],i32[struct:silk/logger:9:3.[18]],i32[struct:silk/logger:9:3.[19]],i32[struct:silk/logger:9:3.[20]],i32[struct:silk/logger:9:3.[21]],i32[struct:silk/logger:9:3.[22]],i32[struct:silk/logger:9:3.[23]],i32[struct:silk/logger:9:3.[24]],i32[struct:silk/logger:9:3.[25]],i32[struct:silk/logger:9:3.[26]],i32[struct:silk/logger:9:3.[27]],i32[struct:silk/logger:9:3.[28]],i32[struct:silk/logger:9:3.[29]],i32[struct:silk/logger:9:3.[30]],i32[struct:silk/logger:9:3.[31]],i32[struct:silk/logger:9:3.[32]],i32[struct:silk/logger:9:3.[33]],i32[struct:silk/logger:9:3.[34]],i32[struct:silk/logger:9:3.[35]],i32[struct:silk/logger:9:3.[36]],i32[struct:silk/logger:9:3.[37]],i32[struct:silk/logger:9:3.[38]],i32[struct:silk/logger:9:3.[39]],i32[struct:silk/logger:9:3.[40]],i32[struct:silk/logger:9:3.[41]],i32[struct:silk/logger:9:3.[42]],i32[struct:silk/logger:9:3.[43]],i32[struct:silk/logger:9:3.[44]],i32[struct:silk/logger:9:3.[45]],i32[struct:silk/logger:9:3.[46]],i32[struct:silk/logger:9:3.[47]],i32[struct:silk/logger:9:3.[48]],i32[struct:silk/logger:9:3.[49]],i32[struct:silk/logger:9:3.[50]],i32[struct:silk/logger:9:3.[51]],i32[struct:silk/logger:9:3.[52]],i32[struct:silk/logger:9:3.[53]],i32[struct:silk/logger:9:3.[54]],i32[struct:silk/logger:9:3.[55]],i32[struct:silk/logger:9:3.[56]],i32[struct:silk/logger:9:3.[57]],i32[struct:silk/logger:9:3.[58]],i32[struct:silk/logger:9:3.[59]],i32[struct:silk/logger:9:3.[60]],i32[struct:silk/logger:9:3.[61]],i32[struct:silk/logger:9:3.[62]],i32[struct:silk/logger:9:3.[63]],usize[struct:silk/logger:9:4],usize[struct:silk/logger:9:5],usize[struct:silk/logger:9:6],bool[struct:silk/logger:9:7],usize[struct:silk/logger:9:8] +calling silk/logger.LogError lanes=1 i32[struct:silk/logger:1:0] calling silk/logger.LogLevel lanes=1 u8[] -calling silk/result.Failure lanes=1 i32[silk/result#1.0.silk/logger#1.0] -calling silk/result.Result lanes=2 i32[silk/result#2.0.tag],i32[silk/result#2.0.payload[0]] -calling silk/result.Success lanes=1 i32[silk/result#0.0] calling &mut silk/logger.InMemoryLogger lanes=1 Address[address] calling &silk/logger.InMemoryLogger lanes=1 Address[address] calling &[u8] lanes=2 Address[address],usize[length] calling string lanes=2 Address[storage],usize[byte-length] calling never lanes=0 -calling silk/result.Failure | silk/result.Success lanes=2 i32[tag],i32[payload[0]] static-data text:6465627567 bytes=6465627567 align=1 address=i64 length=usize:i64 static-data text:6572726f72 bytes=6572726f72 align=1 address=i64 length=usize:i64 static-data text:696e666f bytes=696e666f align=1 address=i64 length=usize:i64 @@ -172,10 +154,10 @@ static-data text:696e666f20616c696173 bytes=696e666f20616c696173 align=1 address static-data text:7365636f6e640a6c696e65 bytes=7365636f6e640a6c696e65 align=1 address=i64 length=usize:i64 static-data text:7472616365 bytes=7472616365 align=1 address=i64 length=usize:i64 static-data text:7761726e696e67 bytes=7761726e696e67 align=1 address=i64 length=usize:i64 -usize-literal 18446744073709551615 bits=64 available [2041, 2049) -usize-literal 0 bits=64 available [2143, 2144) -usize-literal 0 bits=64 available [2359, 2360) -usize-literal 1 bits=64 available [2463, 2464) +usize-literal 18446744073709551615 bits=64 available [2053, 2061) +usize-literal 0 bits=64 available [2155, 2156) +usize-literal 0 bits=64 available [2371, 2372) +usize-literal 1 bits=64 available [2475, 2476) usize-literal 7 bits=64 available [892, 894) usize-literal 0 bits=64 available [936, 938) usize-literal 1 bits=64 available [999, 1001) @@ -192,34 +174,34 @@ usize-literal 6 bits=64 available [1486, 1488) usize-literal 11 bits=64 available [1492, 1495) usize-literal 6 bits=64 available [1537, 1539) usize-literal 6 bits=64 available [1540, 1542) -usize-literal 0 bits=64 available [7050, 7051) -usize-literal 0 bits=64 available [7052, 7054) -usize-literal 0 bits=64 available [7086, 7087) -usize-literal 0 bits=64 available [7088, 7090) -usize-literal 0 bits=64 available [7117, 7118) -usize-literal 0 bits=64 available [7119, 7121) -usize-literal 0 bits=64 available [7170, 7171) -usize-literal 0 bits=64 available [7172, 7174) -usize-literal 0 bits=64 available [6259, 6260) -usize-literal 0 bits=64 available [6261, 6263) -usize-literal 0 bits=64 available [6264, 6266) -usize-literal 0 bits=64 available [6267, 6269) -usize-literal 0 bits=64 available [6270, 6272) -usize-literal 0 bits=64 available [6273, 6275) -usize-literal 0 bits=64 available [6276, 6278) -usize-literal 0 bits=64 available [6279, 6281) -usize-literal 0 bits=64 available [7891, 7892) -usize-literal 1 bits=64 available [7893, 7895) -usize-literal 0 bits=64 available [8010, 8011) -usize-literal 8 bits=64 available [8012, 8014) -usize-literal 0 bits=64 available [8072, 8073) -usize-literal 64 bits=64 available [8074, 8077) -usize-literal 0 bits=64 available [8259, 8260) -usize-literal 0 bits=64 available [8261, 8263) -usize-literal 0 bits=64 available [8382, 8383) -usize-literal 1 bits=64 available [8384, 8386) -usize-literal 0 bits=64 available [8849, 8850) -usize-literal 1 bits=64 available [8851, 8853) +usize-literal 0 bits=64 available [7003, 7004) +usize-literal 0 bits=64 available [7005, 7007) +usize-literal 0 bits=64 available [7039, 7040) +usize-literal 0 bits=64 available [7041, 7043) +usize-literal 0 bits=64 available [7070, 7071) +usize-literal 0 bits=64 available [7072, 7074) +usize-literal 0 bits=64 available [7123, 7124) +usize-literal 0 bits=64 available [7125, 7127) +usize-literal 0 bits=64 available [6212, 6213) +usize-literal 0 bits=64 available [6214, 6216) +usize-literal 0 bits=64 available [6217, 6219) +usize-literal 0 bits=64 available [6220, 6222) +usize-literal 0 bits=64 available [6223, 6225) +usize-literal 0 bits=64 available [6226, 6228) +usize-literal 0 bits=64 available [6229, 6231) +usize-literal 0 bits=64 available [6232, 6234) +usize-literal 0 bits=64 available [7844, 7845) +usize-literal 1 bits=64 available [7846, 7848) +usize-literal 0 bits=64 available [7963, 7964) +usize-literal 8 bits=64 available [7965, 7967) +usize-literal 0 bits=64 available [8025, 8026) +usize-literal 64 bits=64 available [8027, 8030) +usize-literal 0 bits=64 available [8212, 8213) +usize-literal 0 bits=64 available [8214, 8216) +usize-literal 0 bits=64 available [8335, 8336) +usize-literal 1 bits=64 available [8337, 8339) +usize-literal 0 bits=64 available [8802, 8803) +usize-literal 1 bits=64 available [8804, 8806) fn logging/main.main params=0 locals=5 -> i32 entry=r0 r0 operation: %0 = make-effect logging/main.program$effect$-1 captures=none : Effect [1687, 1696) @@ -228,8 +210,8 @@ fn logging/main.main params=0 locals=5 -> i32 entry=r0 return %4 [1666, 1706) fn silk/effect.catchAll7:5:Array?10:RowAlgebra33:8:Concrete20:9:FiniteRow7:5:Array7:5:Array>effectdeclaration:logging/main:programsite:-1, callable@declaration:logging/main:recover> params=2 locals=3 -> Effect entry=r0 r0 operation: - %2 = make-effect silk/effect.catchAll$effect$-1 captures=%0:shared,%1:shared : Effect [14013, 14267) - return %2 [14013, 14267) + %2 = make-effect silk/effect.catchAll$effect$-1 captures=%0:shared,%1:shared : Effect [13853, 13923) + return %2 [13853, 13923) fn logging/main.program params=0 locals=1 -> Effect entry=r0 r0 operation: %0 = make-effect logging/main.program$effect$-1 captures=none : Effect [252, 1577) @@ -241,303 +223,289 @@ fn logging/main.recover params=1 locals=2 -> Effect entry=r0 r1 cleanup: drop %0 cleanup=StructCleanup [1597, 1612) generated return %1 [1620, 1633) -fn silk/effect.result7:5:Array?10:RowAlgebra33:8:Concrete20:9:FiniteRow7:5:Array7:5:Array>effectdeclaration:logging/main:programsite:-1> params=1 locals=2 -> Effect> entry=r0 - r0 operation: - %1 = make-effect silk/effect.result$effect$-1 captures=%0:shared : Effect> [7275, 7331) - return %1 [7275, 7331) fn silk/logger.inMemoryProvider params=0 locals=18 -> silk/logger.InMemoryLogger entry=r0 r0 operation: - %0 = call silk/logger.emptyLevels() : Array [6924, 6938) - %1 = call silk/logger.emptyIndexes() : Array [6952, 6967) - %2 = call silk/logger.emptyIndexes() : Array [6981, 6996) - %3 = call silk/logger.emptyMessages() : Array [7011, 7027) - %4 = literal 0 : usize [7050, 7051) - %5 = literal 0 : usize [7052, 7054) - %6 = call silk/usize.add(%4, %5) : usize [7039, 7055) - %7 = literal 0 : usize [7086, 7087) - %8 = literal 0 : usize [7088, 7090) - %9 = call silk/usize.add(%7, %8) : usize [7075, 7091) - %10 = literal 0 : usize [7117, 7118) - %11 = literal 0 : usize [7119, 7121) - %12 = call silk/usize.add(%10, %11) : usize [7106, 7122) - %13 = literal 0 : bool [7140, 7146) - %14 = literal 0 : usize [7170, 7171) - %15 = literal 0 : usize [7172, 7174) - %16 = call silk/usize.add(%14, %15) : usize [7159, 7175) - %17 = construct silk/logger.InMemoryLogger { #0: %0, #1: %1, #2: %2, #3: %3, #4: %6, #5: %9, #6: %12, #7: %13, #8: %16 } [6895, 7180) - return %17 [6895, 7180) + %0 = call silk/logger.emptyLevels() : Array [6877, 6891) + %1 = call silk/logger.emptyIndexes() : Array [6905, 6920) + %2 = call silk/logger.emptyIndexes() : Array [6934, 6949) + %3 = call silk/logger.emptyMessages() : Array [6964, 6980) + %4 = literal 0 : usize [7003, 7004) + %5 = literal 0 : usize [7005, 7007) + %6 = call silk/usize.add(%4, %5) : usize [6992, 7008) + %7 = literal 0 : usize [7039, 7040) + %8 = literal 0 : usize [7041, 7043) + %9 = call silk/usize.add(%7, %8) : usize [7028, 7044) + %10 = literal 0 : usize [7070, 7071) + %11 = literal 0 : usize [7072, 7074) + %12 = call silk/usize.add(%10, %11) : usize [7059, 7075) + %13 = literal 0 : bool [7093, 7099) + %14 = literal 0 : usize [7123, 7124) + %15 = literal 0 : usize [7125, 7127) + %16 = call silk/usize.add(%14, %15) : usize [7112, 7128) + %17 = construct silk/logger.InMemoryLogger { #0: %0, #1: %1, #2: %2, #3: %3, #4: %6, #5: %9, #6: %12, #7: %13, #8: %16 } [6848, 7133) + return %17 [6848, 7133) fn silk/logger.length params=1 locals=2 -> usize entry=r0 r0 operation: - %1 = read-place %0.#4 : usize [9253, 9264) - return %1 [9253, 9264) + %1 = read-place %0.#4 : usize [9206, 9217) + return %1 [9206, 9217) fn silk/logger.levelAt params=2 locals=5 -> silk/logger.LogLevel entry=r0 r0 operation: - %2 = read-place %0.#0 : Array [9579, 9591) - %3 = move %2 [9564, 9591) - forward r1 [9564, 9591) generated + %2 = read-place %0.#0 : Array [9532, 9544) + %3 = move %2 [9517, 9544) + forward r1 [9517, 9544) generated r1 operation: - %4 = read-place %3[%1/8] : silk/logger.LogLevel [9600, 9614) - forward r2 [9600, 9614) generated + %4 = read-place %3[%1/8] : silk/logger.LogLevel [9553, 9567) + forward r2 [9553, 9567) generated r2 cleanup: - drop %3 [9564, 9591) generated - return %4 [9600, 9614) + drop %3 [9517, 9544) generated + return %4 [9553, 9567) fn silk/logger.messageLengthAt params=2 locals=5 -> usize entry=r0 r0 operation: - %2 = read-place %0.#2 : Array [9927, 9940) - %3 = move %2 [9911, 9940) - forward r1 [9911, 9940) generated + %2 = read-place %0.#2 : Array [9880, 9893) + %3 = move %2 [9864, 9893) + forward r1 [9864, 9893) generated r1 operation: - %4 = read-place %3[%1/8] : usize [9949, 9964) - forward r2 [9949, 9964) generated + %4 = read-place %3[%1/8] : usize [9902, 9917) + forward r2 [9902, 9917) generated r2 cleanup: - drop %3 [9911, 9940) generated - return %4 [9949, 9964) + drop %3 [9864, 9893) generated + return %4 [9902, 9917) fn silk/logger.messageByteAt params=3 locals=21 -> u8 entry=r0 r0 operation: - %3 = read-place %0.#2 : Array [10365, 10378) - %4 = move %3 [10349, 10378) - forward r1 [10349, 10378) generated + %3 = read-place %0.#2 : Array [10318, 10331) + %4 = move %3 [10302, 10331) + forward r1 [10302, 10331) generated r1 operation: - %5 = read-place %4[%1/8] : usize [10393, 10413) - %6 = move %5 [10378, 10413) - forward r2 [10378, 10413) generated + %5 = read-place %4[%1/8] : usize [10346, 10366) + %6 = move %5 [10331, 10366) + forward r2 [10331, 10366) generated r2 operation: - %7 = lessorequal %6, %2 : bool [10418, 10438) - forward r3 [10413, 10459) generated - r3 conditional condition=%7 taken=r4 otherwise=r5 following=r6 [10413, 10459) + %7 = lessorequal %6, %2 : bool [10371, 10391) + forward r3 [10366, 10412) generated + r3 conditional condition=%7 taken=r4 otherwise=r5 following=r6 [10366, 10412) r4 operation: - %8 = literal 1 : i32 [10451, 10453) - %9 = literal 0 : i32 [10455, 10457) - %10 = divide %8, %9 : i32 [10451, 10457) - %11 = move %10 [10440, 10457) - forward r7 [10440, 10457) generated + %8 = literal 1 : i32 [10404, 10406) + %9 = literal 0 : i32 [10408, 10410) + %10 = divide %8, %9 : i32 [10404, 10410) + %11 = move %10 [10393, 10410) + forward r7 [10393, 10410) generated r7 cleanup: - drop %11 [10440, 10457) generated - forward r6 [10413, 10459) generated + drop %11 [10393, 10410) generated + forward r6 [10366, 10412) generated r6 operation: - %12 = read-place %0.#1 : Array [10475, 10488) - %13 = move %12 [10459, 10488) - forward r8 [10459, 10488) generated + %12 = read-place %0.#1 : Array [10428, 10441) + %13 = move %12 [10412, 10441) + forward r8 [10412, 10441) generated r8 operation: - %14 = read-place %13[%1/8] : usize [10503, 10523) - %15 = move %14 [10488, 10523) - forward r9 [10488, 10523) generated + %14 = read-place %13[%1/8] : usize [10456, 10476) + %15 = move %14 [10441, 10476) + forward r9 [10441, 10476) generated r9 operation: - %16 = read-place %0.#3 : Array [10540, 10554) - %17 = move %16 [10523, 10554) - forward r10 [10523, 10554) generated + %16 = read-place %0.#3 : Array [10493, 10507) + %17 = move %16 [10476, 10507) + forward r10 [10476, 10507) generated r10 operation: - %18 = add %15, %2 : usize [10582, 10600) - %19 = read-place %17[%18/64] : i32 [10573, 10601) - %20 = call silk/i32.toU8(%19) : u8 [10563, 10602) - forward r11 [10563, 10602) generated + %18 = add %15, %2 : usize [10535, 10553) + %19 = read-place %17[%18/64] : i32 [10526, 10554) + %20 = call silk/i32.toU8(%19) : u8 [10516, 10555) + forward r11 [10516, 10555) generated r11 cleanup: - drop %17 [10523, 10554) generated - drop %15 [10488, 10523) generated - drop %13 [10459, 10488) generated - drop %6 [10378, 10413) generated - drop %4 [10349, 10378) generated - return %20 [10563, 10602) + drop %17 [10476, 10507) generated + drop %15 [10441, 10476) generated + drop %13 [10412, 10441) generated + drop %6 [10331, 10366) generated + drop %4 [10302, 10331) generated + return %20 [10516, 10555) r5 operation: - forward r6 [10413, 10459) generated + forward r6 [10366, 10412) generated fn silk/effect.provideMut!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logTracesite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.logTrace params=1 locals=2 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %1 = make-effect silk/effect.logTrace$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5288, 5336) - return %1 [5288, 5336) + %1 = make-effect silk/effect.logTrace$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5280, 5328) + return %1 [5280, 5328) fn silk/effect.provideMut!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logDebugsite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.logDebug params=1 locals=2 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %1 = make-effect silk/effect.logDebug$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5504, 5552) - return %1 [5504, 5552) + %1 = make-effect silk/effect.logDebug$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5496, 5544) + return %1 [5496, 5544) fn silk/effect.provideMut!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logsite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.log params=1 locals=2 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %1 = make-effect silk/effect.log$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [4673, 4720) - return %1 [4673, 4720) + %1 = make-effect silk/effect.log$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [4665, 4712) + return %1 [4665, 4712) fn silk/effect.provideMut!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logInfosite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.logInfo params=1 locals=2 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %1 = make-effect silk/effect.logInfo$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5718, 5765) - return %1 [5718, 5765) + %1 = make-effect silk/effect.logInfo$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5710, 5757) + return %1 [5710, 5757) fn silk/effect.provideMut!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logWarningsite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.logWarning params=1 locals=2 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %1 = make-effect silk/effect.logWarning$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5937, 5987) - return %1 [5937, 5987) + %1 = make-effect silk/effect.logWarning$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5929, 5979) + return %1 [5929, 5979) fn silk/effect.provideMut!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logErrorsite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.logError params=1 locals=2 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %1 = make-effect silk/effect.logError$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [6155, 6203) - return %1 [6155, 6203) + %1 = make-effect silk/effect.logError$effect$-1 captures=%0:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [6147, 6195) + return %1 [6147, 6195) fn silk/effect.provideMutstringresult:effect:Shared!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logAtsite:-1> params=2 locals=3 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [23232, 23322) - return %2 [23232, 23322) + %2 = make-effect silk/effect.provideMut$effect$-1 captures=%0:shared,%1:take : once Effect<() ! silk/logger.LogError> [22790, 22880) + return %2 [22790, 22880) fn silk/effect.logAt params=2 locals=3 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = make-effect silk/effect.logAt$effect$-1 captures=%0:copy,%1:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5071, 5120) - return %2 [5071, 5120) + %2 = make-effect silk/effect.logAt$effect$-1 captures=%0:copy,%1:copy : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5063, 5112) + return %2 [5063, 5112) fn silk/logger.emptyLevels params=0 locals=9 -> Array entry=r0 r0 operation: - %0 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6050, 6069) - %1 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6070, 6089) - %2 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6090, 6109) - %3 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6110, 6129) - %4 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6130, 6149) - %5 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6150, 6169) - %6 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6170, 6189) - %7 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6190, 6209) - %8 = construct-array Array [%0, %1, %2, %3, %4, %5, %6, %7] [6048, 6213) - return %8 [6048, 6213) + %0 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6003, 6022) + %1 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6023, 6042) + %2 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6043, 6062) + %3 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6063, 6082) + %4 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6083, 6102) + %5 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6103, 6122) + %6 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6123, 6142) + %7 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [6143, 6162) + %8 = construct-array Array [%0, %1, %2, %3, %4, %5, %6, %7] [6001, 6166) + return %8 [6001, 6166) fn silk/logger.emptyIndexes params=0 locals=9 -> Array entry=r0 r0 operation: - %0 = literal 0 : usize [6259, 6260) - %1 = literal 0 : usize [6261, 6263) - %2 = literal 0 : usize [6264, 6266) - %3 = literal 0 : usize [6267, 6269) - %4 = literal 0 : usize [6270, 6272) - %5 = literal 0 : usize [6273, 6275) - %6 = literal 0 : usize [6276, 6278) - %7 = literal 0 : usize [6279, 6281) - %8 = construct-array Array [%0, %1, %2, %3, %4, %5, %6, %7] [6257, 6282) - return %8 [6257, 6282) + %0 = literal 0 : usize [6212, 6213) + %1 = literal 0 : usize [6214, 6216) + %2 = literal 0 : usize [6217, 6219) + %3 = literal 0 : usize [6220, 6222) + %4 = literal 0 : usize [6223, 6225) + %5 = literal 0 : usize [6226, 6228) + %6 = literal 0 : usize [6229, 6231) + %7 = literal 0 : usize [6232, 6234) + %8 = construct-array Array [%0, %1, %2, %3, %4, %5, %6, %7] [6210, 6235) + return %8 [6210, 6235) fn silk/logger.emptyMessages params=0 locals=65 -> Array entry=r0 r0 operation: - %0 = literal 0 : i32 [6330, 6336) - %1 = literal 0 : i32 [6337, 6339) - %2 = literal 0 : i32 [6340, 6342) - %3 = literal 0 : i32 [6343, 6345) - %4 = literal 0 : i32 [6346, 6348) - %5 = literal 0 : i32 [6349, 6351) - %6 = literal 0 : i32 [6352, 6354) - %7 = literal 0 : i32 [6355, 6357) - %8 = literal 0 : i32 [6358, 6364) - %9 = literal 0 : i32 [6365, 6367) - %10 = literal 0 : i32 [6368, 6370) - %11 = literal 0 : i32 [6371, 6373) - %12 = literal 0 : i32 [6374, 6376) - %13 = literal 0 : i32 [6377, 6379) - %14 = literal 0 : i32 [6380, 6382) - %15 = literal 0 : i32 [6383, 6385) - %16 = literal 0 : i32 [6386, 6392) - %17 = literal 0 : i32 [6393, 6395) - %18 = literal 0 : i32 [6396, 6398) - %19 = literal 0 : i32 [6399, 6401) - %20 = literal 0 : i32 [6402, 6404) - %21 = literal 0 : i32 [6405, 6407) - %22 = literal 0 : i32 [6408, 6410) - %23 = literal 0 : i32 [6411, 6413) - %24 = literal 0 : i32 [6414, 6420) - %25 = literal 0 : i32 [6421, 6423) - %26 = literal 0 : i32 [6424, 6426) - %27 = literal 0 : i32 [6427, 6429) - %28 = literal 0 : i32 [6430, 6432) - %29 = literal 0 : i32 [6433, 6435) - %30 = literal 0 : i32 [6436, 6438) - %31 = literal 0 : i32 [6439, 6441) - %32 = literal 0 : i32 [6442, 6448) - %33 = literal 0 : i32 [6449, 6451) - %34 = literal 0 : i32 [6452, 6454) - %35 = literal 0 : i32 [6455, 6457) - %36 = literal 0 : i32 [6458, 6460) - %37 = literal 0 : i32 [6461, 6463) - %38 = literal 0 : i32 [6464, 6466) - %39 = literal 0 : i32 [6467, 6469) - %40 = literal 0 : i32 [6470, 6476) - %41 = literal 0 : i32 [6477, 6479) - %42 = literal 0 : i32 [6480, 6482) - %43 = literal 0 : i32 [6483, 6485) - %44 = literal 0 : i32 [6486, 6488) - %45 = literal 0 : i32 [6489, 6491) - %46 = literal 0 : i32 [6492, 6494) - %47 = literal 0 : i32 [6495, 6497) - %48 = literal 0 : i32 [6498, 6504) - %49 = literal 0 : i32 [6505, 6507) - %50 = literal 0 : i32 [6508, 6510) - %51 = literal 0 : i32 [6511, 6513) - %52 = literal 0 : i32 [6514, 6516) - %53 = literal 0 : i32 [6517, 6519) - %54 = literal 0 : i32 [6520, 6522) - %55 = literal 0 : i32 [6523, 6525) - %56 = literal 0 : i32 [6526, 6532) - %57 = literal 0 : i32 [6533, 6535) - %58 = literal 0 : i32 [6536, 6538) - %59 = literal 0 : i32 [6539, 6541) - %60 = literal 0 : i32 [6542, 6544) - %61 = literal 0 : i32 [6545, 6547) - %62 = literal 0 : i32 [6548, 6550) - %63 = literal 0 : i32 [6551, 6553) - %64 = construct-array Array [%0, %1, %2, %3, %4, %5, %6, %7, %8, %9, %10, %11, %12, %13, %14, %15, %16, %17, %18, %19, %20, %21, %22, %23, %24, %25, %26, %27, %28, %29, %30, %31, %32, %33, %34, %35, %36, %37, %38, %39, %40, %41, %42, %43, %44, %45, %46, %47, %48, %49, %50, %51, %52, %53, %54, %55, %56, %57, %58, %59, %60, %61, %62, %63] [6328, 6558) - return %64 [6328, 6558) + %0 = literal 0 : i32 [6283, 6289) + %1 = literal 0 : i32 [6290, 6292) + %2 = literal 0 : i32 [6293, 6295) + %3 = literal 0 : i32 [6296, 6298) + %4 = literal 0 : i32 [6299, 6301) + %5 = literal 0 : i32 [6302, 6304) + %6 = literal 0 : i32 [6305, 6307) + %7 = literal 0 : i32 [6308, 6310) + %8 = literal 0 : i32 [6311, 6317) + %9 = literal 0 : i32 [6318, 6320) + %10 = literal 0 : i32 [6321, 6323) + %11 = literal 0 : i32 [6324, 6326) + %12 = literal 0 : i32 [6327, 6329) + %13 = literal 0 : i32 [6330, 6332) + %14 = literal 0 : i32 [6333, 6335) + %15 = literal 0 : i32 [6336, 6338) + %16 = literal 0 : i32 [6339, 6345) + %17 = literal 0 : i32 [6346, 6348) + %18 = literal 0 : i32 [6349, 6351) + %19 = literal 0 : i32 [6352, 6354) + %20 = literal 0 : i32 [6355, 6357) + %21 = literal 0 : i32 [6358, 6360) + %22 = literal 0 : i32 [6361, 6363) + %23 = literal 0 : i32 [6364, 6366) + %24 = literal 0 : i32 [6367, 6373) + %25 = literal 0 : i32 [6374, 6376) + %26 = literal 0 : i32 [6377, 6379) + %27 = literal 0 : i32 [6380, 6382) + %28 = literal 0 : i32 [6383, 6385) + %29 = literal 0 : i32 [6386, 6388) + %30 = literal 0 : i32 [6389, 6391) + %31 = literal 0 : i32 [6392, 6394) + %32 = literal 0 : i32 [6395, 6401) + %33 = literal 0 : i32 [6402, 6404) + %34 = literal 0 : i32 [6405, 6407) + %35 = literal 0 : i32 [6408, 6410) + %36 = literal 0 : i32 [6411, 6413) + %37 = literal 0 : i32 [6414, 6416) + %38 = literal 0 : i32 [6417, 6419) + %39 = literal 0 : i32 [6420, 6422) + %40 = literal 0 : i32 [6423, 6429) + %41 = literal 0 : i32 [6430, 6432) + %42 = literal 0 : i32 [6433, 6435) + %43 = literal 0 : i32 [6436, 6438) + %44 = literal 0 : i32 [6439, 6441) + %45 = literal 0 : i32 [6442, 6444) + %46 = literal 0 : i32 [6445, 6447) + %47 = literal 0 : i32 [6448, 6450) + %48 = literal 0 : i32 [6451, 6457) + %49 = literal 0 : i32 [6458, 6460) + %50 = literal 0 : i32 [6461, 6463) + %51 = literal 0 : i32 [6464, 6466) + %52 = literal 0 : i32 [6467, 6469) + %53 = literal 0 : i32 [6470, 6472) + %54 = literal 0 : i32 [6473, 6475) + %55 = literal 0 : i32 [6476, 6478) + %56 = literal 0 : i32 [6479, 6485) + %57 = literal 0 : i32 [6486, 6488) + %58 = literal 0 : i32 [6489, 6491) + %59 = literal 0 : i32 [6492, 6494) + %60 = literal 0 : i32 [6495, 6497) + %61 = literal 0 : i32 [6498, 6500) + %62 = literal 0 : i32 [6501, 6503) + %63 = literal 0 : i32 [6504, 6506) + %64 = construct-array Array [%0, %1, %2, %3, %4, %5, %6, %7, %8, %9, %10, %11, %12, %13, %14, %15, %16, %17, %18, %19, %20, %21, %22, %23, %24, %25, %26, %27, %28, %29, %30, %31, %32, %33, %34, %35, %36, %37, %38, %39, %40, %41, %42, %43, %44, %45, %46, %47, %48, %49, %50, %51, %52, %53, %54, %55, %56, %57, %58, %59, %60, %61, %62, %63] [6281, 6511) + return %64 [6281, 6511) fn silk/usize.add params=2 locals=3 -> usize entry=r0 r0 operation: - %2 = add %0, %1 : usize [7559, 7591) - return %2 [7559, 7591) + %2 = add %0, %1 : usize [7823, 7855) + return %2 [7823, 7855) fn silk/i32.toU8 params=1 locals=2 -> u8 entry=r0 r0 operation: - %1 = convert %0 i32 -> u8 [3078, 3103) - return %1 [3078, 3103) + %1 = convert %0 i32 -> u8 [3090, 3115) + return %1 [3090, 3115) fn silk/logger.record params=3 locals=4 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %3 = make-effect silk/logger.record$effect$-1 captures=%0:take,%1:copy,%2:copy : once Effect<() ! silk/logger.LogError> [7774, 8926) - return %3 [7774, 8926) + %3 = make-effect silk/logger.record$effect$-1 captures=%0:take,%1:copy,%2:copy : once Effect<() ! silk/logger.LogError> [7727, 8879) + return %3 [7727, 8879) fn silk/string.utf8Bytes params=1 locals=2 -> &[u8] entry=r0 r0 operation: - %1 = string-utf8-bytes %0 loans=none : &[u8] [10442, 10475) - return %1 [10442, 10475) + %1 = string-utf8-bytes %0 loans=none : &[u8] [10507, 10540) + return %1 [10507, 10540) fn silk/u8.toI32 params=1 locals=2 -> i32 entry=r0 r0 operation: - %1 = convert %0 u8 -> i32 [4782, 4807) - return %1 [4782, 4807) + %1 = convert %0 u8 -> i32 [4969, 4994) + return %1 [4969, 4994) fn silk/logger.reject params=1 locals=2 -> Effect entry=r0 r0 operation: - %1 = make-effect silk/logger.reject$effect$-1 captures=%0:copy : Effect [3001, 3036) - return %1 [3001, 3036) -fn silk/effect.catchAll$effect$-17:5:Array?10:RowAlgebra33:8:Concrete20:9:FiniteRow7:5:Array7:5:Array>effectdeclaration:logging/main:programsite:-1, callable@declaration:logging/main:recover> params=2 locals=15 -> Effect entry=r0 + %1 = make-effect silk/logger.reject$effect$-1 captures=%0:copy : Effect [3013, 3048) + return %1 [3013, 3048) +fn silk/effect.catchAll$effect$-17:5:Array?10:RowAlgebra33:8:Concrete20:9:FiniteRow7:5:Array7:5:Array>effectdeclaration:logging/main:programsite:-1, callable@declaration:logging/main:recover> params=2 locals=13 -> Effect entry=r0 r0 operation: - %4 = run-static-effect runner=silk/effect.result$effect$-1 captures=%0:shared arguments=none propagate= : silk/result.Result [14033, 14055) - %5 = move %4 [14015, 14055) - forward r1 [14015, 14055) generated - r1 operation: - %13 = match#14064 move %5 : silk/result.Result -> i32 [14064, 14265) - members silk/result.Result - decision silk/result.Result candidates=#0 - arm #0 silk/result.Result before=silk/result.Result after=empty [14087, 14261) - bind #0 %6 <- #0 : silk/result.Failure | silk/result.Success access=Move [14106, 14121) - selected access=Move result=%12 end-borrow=false - %12 = match#14126 move %6 : silk/result.Failure | silk/result.Success -> i32 [14126, 14261) - members silk/result.Failure, silk/result.Success - decision silk/result.Failure candidates=#1 - decision silk/result.Success candidates=#0 - arm #0 silk/result.Success before=silk/result.Failure,silk/result.Success after=silk/result.Failure [14147, 14199) - bind #0 %7 <- #0 : i32 access=Move [14166, 14181) - selected access=Move result=%7 end-borrow=false - arm #1 silk/result.Failure before=silk/result.Failure after=empty [14199, 14255) - bind #0 %8 <- #0 : silk/logger.LogError access=Move [14218, 14224) - selected access=Move result=%11 end-borrow=false - %9 = apply-callable %1(%8) captures=none access=shared evaluation=CalleeThenArguments realization=Environment : Effect [14233, 14255) - %11 = run-effect-value %9 runner=logging/main.recover$effect$-1 providers=none arguments=none propagate= : i32 [14229, 14255) - %14 = effect-outcome tag=0 %13 : Effect [14064, 14265) - return %14 [14064, 14265) + %3 = catch-effect %0 runner=logging/main.program$effect$-1 arguments=none : bool [13868, 13921) + %11 = conditional %3 : i32 [13868, 13921) generated + taken -> %4 + otherwise -> %10 + %10 = match#13868 move %5 : silk/logger.LogError -> i32 [13868, 13921) generated + members silk/logger.LogError + decision silk/logger.LogError candidates=#0 + arm #0 silk/logger.LogError before=silk/logger.LogError after=empty [13868, 13921) generated + bind #0 %6 <- payload : silk/logger.LogError access=Move [13868, 13921) generated + selected access=Move result=%9 end-borrow=false + %7 = apply-callable %1(%6) captures=none access=shared evaluation=CalleeThenArguments realization=Environment : Effect [13868, 13921) generated + %9 = run-effect-value %7 runner=logging/main.recover$effect$-1 providers=none arguments=none propagate= : i32 [13864, 13921) + %12 = effect-outcome tag=0 %11 : Effect [13864, 13921) + return %12 [13864, 13921) fn logging/main.program$effect$-1 params=0 locals=132 -> Effect entry=r0 r0 operation: %0 = call silk/logger.inMemoryProvider() : silk/logger.InMemoryLogger [273, 299) @@ -546,42 +514,42 @@ fn logging/main.program$effect$-1 params=0 locals=132 -> Effect1 : () failure-loans=l1:%2 releases=%1 [313, 374) + %6 = run-static-effect runner=silk/effect.logTrace$effect$-1$provided$19 captures=%3:copy arguments=%2 propagate=1->1 : () failure-loans=l1:%2 releases=%1 [313, 374) end-loan l1 %2 [313, 374) generated %7 = move %6 [299, 374) forward r2 [299, 374) generated r2 operation: %8 = begin-loan l1 exclusive %1 source=silk/logger.InMemoryLogger : &mut silk/logger.InMemoryLogger reborrow=false suspended=false [436, 448) %9 = static-string text:6465627567 byte-length=5 : string [427, 434) - %12 = run-static-effect runner=silk/effect.logDebug$effect$-1$provided$21 captures=%9:copy arguments=%8 propagate=1->1 : () failure-loans=l1:%8 releases=%1 [388, 449) + %12 = run-static-effect runner=silk/effect.logDebug$effect$-1$provided$20 captures=%9:copy arguments=%8 propagate=1->1 : () failure-loans=l1:%8 releases=%1 [388, 449) end-loan l1 %8 [388, 449) generated %13 = move %12 [374, 449) forward r3 [374, 449) generated r3 operation: %14 = begin-loan l1 exclusive %1 source=silk/logger.InMemoryLogger : &mut silk/logger.InMemoryLogger reborrow=false suspended=false [504, 516) %15 = static-string text:696e666f byte-length=4 : string [496, 502) - %18 = run-static-effect runner=silk/effect.log$effect$-1$provided$22 captures=%15:copy arguments=%14 propagate=1->1 : () failure-loans=l1:%14 releases=%1 [462, 517) + %18 = run-static-effect runner=silk/effect.log$effect$-1$provided$21 captures=%15:copy arguments=%14 propagate=1->1 : () failure-loans=l1:%14 releases=%1 [462, 517) end-loan l1 %14 [462, 517) generated %19 = move %18 [449, 517) forward r4 [449, 517) generated r4 operation: %20 = begin-loan l1 exclusive %1 source=silk/logger.InMemoryLogger : &mut silk/logger.InMemoryLogger reborrow=false suspended=false [587, 599) %21 = static-string text:696e666f20616c696173 byte-length=10 : string [573, 585) - %24 = run-static-effect runner=silk/effect.logInfo$effect$-1$provided$23 captures=%21:copy arguments=%20 propagate=1->1 : () failure-loans=l1:%20 releases=%1 [535, 600) + %24 = run-static-effect runner=silk/effect.logInfo$effect$-1$provided$22 captures=%21:copy arguments=%20 propagate=1->1 : () failure-loans=l1:%20 releases=%1 [535, 600) end-loan l1 %20 [535, 600) generated %25 = move %24 [517, 600) forward r5 [517, 600) generated r5 operation: %26 = begin-loan l1 exclusive %1 source=silk/logger.InMemoryLogger : &mut silk/logger.InMemoryLogger reborrow=false suspended=false [668, 680) %27 = static-string text:7761726e696e67 byte-length=7 : string [657, 666) - %30 = run-static-effect runner=silk/effect.logWarning$effect$-1$provided$24 captures=%27:copy arguments=%26 propagate=1->1 : () failure-loans=l1:%26 releases=%1 [616, 681) + %30 = run-static-effect runner=silk/effect.logWarning$effect$-1$provided$23 captures=%27:copy arguments=%26 propagate=1->1 : () failure-loans=l1:%26 releases=%1 [616, 681) end-loan l1 %26 [616, 681) generated %31 = move %30 [600, 681) forward r6 [600, 681) generated r6 operation: %32 = begin-loan l1 exclusive %1 source=silk/logger.InMemoryLogger : &mut silk/logger.InMemoryLogger reborrow=false suspended=false [743, 755) %33 = static-string text:6572726f72 byte-length=5 : string [734, 741) - %36 = run-static-effect runner=silk/effect.logError$effect$-1$provided$25 captures=%33:copy arguments=%32 propagate=1->1 : () failure-loans=l1:%32 releases=%1 [695, 756) + %36 = run-static-effect runner=silk/effect.logError$effect$-1$provided$24 captures=%33:copy arguments=%32 propagate=1->1 : () failure-loans=l1:%32 releases=%1 [695, 756) end-loan l1 %32 [695, 756) generated %37 = move %36 [681, 756) forward r7 [681, 756) generated @@ -589,7 +557,7 @@ fn logging/main.program$effect$-1 params=0 locals=132 -> Effect1 : () failure-loans=l1:%38 releases=%1 [773, 868) + %43 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%39:copy,%40:copy arguments=%38 propagate=1->1 : () failure-loans=l1:%38 releases=%1 [773, 868) end-loan l1 %38 [773, 868) generated %44 = move %43 [756, 868) forward r8 [756, 868) generated @@ -913,329 +881,324 @@ fn logging/main.recover$effect$-1 params=0 locals=2 -> Effect entry=r0 %0 = literal 0 : i32 [1629, 1631) %1 = effect-outcome tag=0 %0 : Effect [1629, 1631) return %1 [1629, 1631) -fn silk/effect.result$effect$-17:5:Array?10:RowAlgebra33:8:Concrete20:9:FiniteRow7:5:Array7:5:Array>effectdeclaration:logging/main:programsite:-1> params=1 locals=4 -> Effect> entry=r0 - r0 operation: - %2 = effect-result %0 runner=logging/main.program$effect$-1 arguments=none : silk/result.Result [7286, 7329) - %3 = effect-outcome tag=0 %2 : Effect> [7286, 7329) - return %3 [7286, 7329) fn silk/effect.provideMut$effect$-1!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logTracesite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.logTrace$effect$-1$provided$20 base=silk/effect.logTrace$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.logTrace$effect$-1$provided$19 base=silk/effect.logTrace$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/effect.provideMut$effect$-1!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logDebugsite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.logDebug$effect$-1$provided$21 base=silk/effect.logDebug$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.logDebug$effect$-1$provided$20 base=silk/effect.logDebug$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/effect.provideMut$effect$-1!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logsite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.log$effect$-1$provided$22 base=silk/effect.log$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.log$effect$-1$provided$21 base=silk/effect.log$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/effect.provideMut$effect$-1!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logInfosite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.logInfo$effect$-1$provided$23 base=silk/effect.logInfo$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.logInfo$effect$-1$provided$22 base=silk/effect.logInfo$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/effect.provideMut$effect$-1!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logWarningsite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.logWarning$effect$-1$provided$24 base=silk/effect.logWarning$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.logWarning$effect$-1$provided$23 base=silk/effect.logWarning$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/effect.provideMut$effect$-1!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logErrorsite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.logError$effect$-1$provided$25 base=silk/effect.logError$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.logError$effect$-1$provided$24 base=silk/effect.logError$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/effect.provideMut$effect$-1stringresult:effect:Shared!10:RowAlgebra67:8:Concrete54:9:FiniteRow40:5:Array30:nominal:silk/logger.LogError<>7:5:Array?10:RowAlgebra87:8:Concrete74:9:FiniteRow60:5:Array50:nominal:silk/logger.Logger<>@DefaultRole:Exclusive7:5:Array>effectdeclaration:silk/effect:logAtsite:-1> params=2 locals=5 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - forward r1 [23234, 23301) generated + forward r1 [22792, 22859) generated r1 operation: - %3 = run-effect-value %0 runner=silk/effect.logAt$effect$-1$provided$26 base=silk/effect.logAt$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [23310, 23320) - %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [23310, 23320) - forward r2 [23310, 23320) generated + %3 = run-effect-value %0 runner=silk/effect.logAt$effect$-1$provided$25 base=silk/effect.logAt$effect$-1 providers=silk/logger.Logger@DefaultRole:exclusive:exclusive arguments=%1 propagate=1->1 : () [22868, 22878) + %4 = effect-outcome tag=0 %3 : once Effect<() ! silk/logger.LogError> [22868, 22878) + forward r2 [22868, 22878) generated r2 cleanup: - drop %1 [23155, 23174) generated - return %4 [23310, 23320) + drop %1 [22713, 22732) generated + return %4 [22868, 22878) fn silk/logger.record$effect$-1 params=3 locals=83 -> once Effect<() ! silk/logger.LogError> entry=r0 r0 operation: - %3 = call silk/string.utf8Bytes(%2) : &[u8] [7791, 7816) - %4 = move %3 [7776, 7816) - forward r1 [7776, 7816) generated + %3 = call silk/string.utf8Bytes(%2) : &[u8] [7744, 7769) + %4 = move %3 [7729, 7769) + forward r1 [7729, 7769) generated r1 operation: - %5 = read-place %0.#6 : usize [7832, 7846) - %6 = move %5 [7816, 7846) - forward r2 [7816, 7846) generated + %5 = read-place %0.#6 : usize [7785, 7799) + %6 = move %5 [7769, 7799) + forward r2 [7769, 7799) generated r2 operation: - check-place %0.#6 : usize [7846, 7862) - %7 = read-place %0.#6 : usize [7864, 7878) - %8 = literal 0 : usize [7891, 7892) - %9 = literal 1 : usize [7893, 7895) - %10 = call silk/usize.add(%8, %9) : usize [7880, 7896) - %11 = add %7, %10 : usize [7864, 7896) - write-place %0.#6 <- %11 : usize replacement=Copy commit=AfterCleanup [7846, 7896) - forward r3 [7846, 7896) generated + check-place %0.#6 : usize [7799, 7815) + %7 = read-place %0.#6 : usize [7817, 7831) + %8 = literal 0 : usize [7844, 7845) + %9 = literal 1 : usize [7846, 7848) + %10 = call silk/usize.add(%8, %9) : usize [7833, 7849) + %11 = add %7, %10 : usize [7817, 7849) + write-place %0.#6 <- %11 : usize replacement=Copy commit=AfterCleanup [7799, 7849) + forward r3 [7799, 7849) generated r3 operation: - %12 = read-place %0.#7 : bool [7901, 7918) - forward r4 [7896, 7979) generated - r4 conditional condition=%12 taken=r5 otherwise=r6 following=r7 [7896, 7979) + %12 = read-place %0.#7 : bool [7854, 7871) + forward r4 [7849, 7932) generated + r4 conditional condition=%12 taken=r5 otherwise=r6 following=r7 [7849, 7932) r5 operation: - %13 = read-place %0.#8 : usize [7938, 7950) - %14 = equals %6, %13 : bool [7927, 7950) - forward r8 [7920, 7975) generated - r8 conditional condition=%14 taken=r9 otherwise=r10 following=r11 [7920, 7975) + %13 = read-place %0.#8 : usize [7891, 7903) + %14 = equals %6, %13 : bool [7880, 7903) + forward r8 [7873, 7928) generated + r8 conditional condition=%14 taken=r9 otherwise=r10 following=r11 [7873, 7928) r9 operation: - %15 = literal 1 : i32 [7971, 7972) - %18 = run-static-effect runner=silk/logger.reject$effect$-1 captures=%15:copy arguments=none propagate=1->1 : never [7959, 7973) - %19 = effect-outcome tag=0 %18 : once Effect<() ! silk/logger.LogError> [7959, 7973) - forward r12 [7959, 7973) generated + %15 = literal 1 : i32 [7924, 7925) + %18 = run-static-effect runner=silk/logger.reject$effect$-1 captures=%15:copy arguments=none propagate=1->1 : never [7912, 7926) + %19 = effect-outcome tag=0 %18 : once Effect<() ! silk/logger.LogError> [7912, 7926) + forward r12 [7912, 7926) generated r12 cleanup: - drop %6 [7816, 7846) generated - drop %4 [7776, 7816) generated - drop %0 [7689, 7717) generated - return %19 [7959, 7973) + drop %6 [7769, 7799) generated + drop %4 [7729, 7769) generated + drop %0 [7642, 7670) generated + return %19 [7912, 7926) r10 operation: - forward r11 [7920, 7975) generated + forward r11 [7873, 7928) generated r11 operation: - forward r7 [7896, 7979) generated + forward r7 [7849, 7932) generated r7 operation: - %20 = read-place %0.#4 : usize [7985, 7996) - %21 = literal 0 : usize [8010, 8011) - %22 = literal 8 : usize [8012, 8014) - %23 = call silk/usize.add(%21, %22) : usize [7999, 8015) - %24 = equals %20, %23 : bool [7985, 8015) - forward r13 [7979, 8040) generated - r13 conditional condition=%24 taken=r14 otherwise=r15 following=r16 [7979, 8040) + %20 = read-place %0.#4 : usize [7938, 7949) + %21 = literal 0 : usize [7963, 7964) + %22 = literal 8 : usize [7965, 7967) + %23 = call silk/usize.add(%21, %22) : usize [7952, 7968) + %24 = equals %20, %23 : bool [7938, 7968) + forward r13 [7932, 7993) generated + r13 conditional condition=%24 taken=r14 otherwise=r15 following=r16 [7932, 7993) r14 operation: - %25 = literal 2 : i32 [8036, 8037) - %28 = run-static-effect runner=silk/logger.reject$effect$-1 captures=%25:copy arguments=none propagate=1->1 : never [8024, 8038) - %29 = effect-outcome tag=0 %28 : once Effect<() ! silk/logger.LogError> [8024, 8038) - forward r17 [8024, 8038) generated + %25 = literal 2 : i32 [7989, 7990) + %28 = run-static-effect runner=silk/logger.reject$effect$-1 captures=%25:copy arguments=none propagate=1->1 : never [7977, 7991) + %29 = effect-outcome tag=0 %28 : once Effect<() ! silk/logger.LogError> [7977, 7991) + forward r17 [7977, 7991) generated r17 cleanup: - drop %6 [7816, 7846) generated - drop %4 [7776, 7816) generated - drop %0 [7689, 7717) generated - return %29 [8024, 8038) + drop %6 [7769, 7799) generated + drop %4 [7729, 7769) generated + drop %0 [7642, 7670) generated + return %29 [7977, 7991) r15 operation: - forward r16 [7979, 8040) generated + forward r16 [7932, 7993) generated r16 operation: - %30 = slice-length %4 : i32 [8045, 8059) - %31 = literal 0 : usize [8072, 8073) - %32 = literal 64 : usize [8074, 8077) - %33 = call silk/usize.add(%31, %32) : usize [8061, 8078) - %34 = read-place %0.#5 : usize [8080, 8099) - %35 = subtract %33, %34 : usize [8061, 8099) - %36 = greaterthan %30, %35 : bool [8045, 8099) - forward r18 [8040, 8124) generated - r18 conditional condition=%36 taken=r19 otherwise=r20 following=r21 [8040, 8124) + %30 = slice-length %4 : i32 [7998, 8012) + %31 = literal 0 : usize [8025, 8026) + %32 = literal 64 : usize [8027, 8030) + %33 = call silk/usize.add(%31, %32) : usize [8014, 8031) + %34 = read-place %0.#5 : usize [8033, 8052) + %35 = subtract %33, %34 : usize [8014, 8052) + %36 = greaterthan %30, %35 : bool [7998, 8052) + forward r18 [7993, 8077) generated + r18 conditional condition=%36 taken=r19 otherwise=r20 following=r21 [7993, 8077) r19 operation: - %37 = literal 2 : i32 [8120, 8121) - %40 = run-static-effect runner=silk/logger.reject$effect$-1 captures=%37:copy arguments=none propagate=1->1 : never [8108, 8122) - %41 = effect-outcome tag=0 %40 : once Effect<() ! silk/logger.LogError> [8108, 8122) - forward r22 [8108, 8122) generated + %37 = literal 2 : i32 [8073, 8074) + %40 = run-static-effect runner=silk/logger.reject$effect$-1 captures=%37:copy arguments=none propagate=1->1 : never [8061, 8075) + %41 = effect-outcome tag=0 %40 : once Effect<() ! silk/logger.LogError> [8061, 8075) + forward r22 [8061, 8075) generated r22 cleanup: - drop %6 [7816, 7846) generated - drop %4 [7776, 7816) generated - drop %0 [7689, 7717) generated - return %41 [8108, 8122) + drop %6 [7769, 7799) generated + drop %4 [7729, 7769) generated + drop %0 [7642, 7670) generated + return %41 [8061, 8075) r20 operation: - forward r21 [8040, 8124) generated + forward r21 [7993, 8077) generated r21 operation: - %42 = read-place %0.#5 : usize [8140, 8159) - %43 = move %42 [8124, 8159) - forward r23 [8124, 8159) generated + %42 = read-place %0.#5 : usize [8093, 8112) + %43 = move %42 [8077, 8112) + forward r23 [8077, 8112) generated r23 operation: - check-place %0.#3 : Array [8199, 8212) - %44 = call silk/logger.emptyMessages() : Array [8213, 8229) - %45 = read-place consume %0.#3 : Array [8180, 8230) - write-place %0.#3 <- %44 : Array replacement=Copy commit=AfterCleanup [8180, 8230) - %46 = move %45 [8159, 8230) - forward r24 [8159, 8230) generated + check-place %0.#3 : Array [8152, 8165) + %44 = call silk/logger.emptyMessages() : Array [8166, 8182) + %45 = read-place consume %0.#3 : Array [8133, 8183) + write-place %0.#3 <- %44 : Array replacement=Copy commit=AfterCleanup [8133, 8183) + %46 = move %45 [8112, 8183) + forward r24 [8112, 8183) generated r24 operation: - %47 = literal 0 : usize [8259, 8260) - %48 = literal 0 : usize [8261, 8263) - %49 = call silk/usize.add(%47, %48) : usize [8248, 8264) - %50 = move %49 [8230, 8264) - forward r25 [8230, 8264) generated - r25 loop loop0 condition=r26 value=%52 body=r27 following=r28 [8264, 8391) + %47 = literal 0 : usize [8212, 8213) + %48 = literal 0 : usize [8214, 8216) + %49 = call silk/usize.add(%47, %48) : usize [8201, 8217) + %50 = move %49 [8183, 8217) + forward r25 [8183, 8217) generated + r25 loop loop0 condition=r26 value=%52 body=r27 following=r28 [8217, 8344) r26 operation owner=loop0: - %51 = slice-length %4 : i32 [8280, 8294) - %52 = lessthan %50, %51 : bool [8272, 8294) - yield [8264, 8391) generated + %51 = slice-length %4 : i32 [8233, 8247) + %52 = lessthan %50, %51 : bool [8225, 8247) + yield [8217, 8344) generated r27 operation owner=loop0: - %53 = add %43, %50 : usize [8310, 8324) - check-place %46[%53/64] : i32 [8296, 8325) - %54 = read-place %4[%50/slice:shared] : u8 [8337, 8350) - %55 = call silk/u8.toI32(%54) : i32 [8327, 8351) - write-place %46[%53/64] <- %55 : i32 replacement=Copy commit=AfterCleanup [8296, 8351) - forward r29 [8296, 8351) generated + %53 = add %43, %50 : usize [8263, 8277) + check-place %46[%53/64] : i32 [8249, 8278) + %54 = read-place %4[%50/slice:shared] : u8 [8290, 8303) + %55 = call silk/u8.toI32(%54) : i32 [8280, 8304) + write-place %46[%53/64] <- %55 : i32 replacement=Copy commit=AfterCleanup [8249, 8304) + forward r29 [8249, 8304) generated r29 operation owner=loop0: - check-place %50 : usize [8351, 8361) - %56 = literal 0 : usize [8382, 8383) - %57 = literal 1 : usize [8384, 8386) - %58 = call silk/usize.add(%56, %57) : usize [8371, 8387) - %59 = add %50, %58 : usize [8363, 8387) - write-place %50 <- %59 : usize replacement=Copy commit=AfterCleanup [8351, 8387) - forward r30 [8351, 8387) generated + check-place %50 : usize [8304, 8314) + %56 = literal 0 : usize [8335, 8336) + %57 = literal 1 : usize [8337, 8339) + %58 = call silk/usize.add(%56, %57) : usize [8324, 8340) + %59 = add %50, %58 : usize [8316, 8340) + write-place %50 <- %59 : usize replacement=Copy commit=AfterCleanup [8304, 8340) + forward r30 [8304, 8340) generated r30 operation owner=loop0: - repeat loop0 [8264, 8391) generated + repeat loop0 [8217, 8344) generated r28 operation: - check-place %0.#0 : Array [8429, 8440) - %60 = call silk/logger.emptyLevels() : Array [8441, 8455) - %61 = read-place consume %0.#0 : Array [8410, 8456) - write-place %0.#0 <- %60 : Array replacement=Copy commit=AfterCleanup [8410, 8456) - %62 = move %61 [8391, 8456) - forward r31 [8391, 8456) generated + check-place %0.#0 : Array [8382, 8393) + %60 = call silk/logger.emptyLevels() : Array [8394, 8408) + %61 = read-place consume %0.#0 : Array [8363, 8409) + write-place %0.#0 <- %60 : Array replacement=Copy commit=AfterCleanup [8363, 8409) + %62 = move %61 [8344, 8409) + forward r31 [8344, 8409) generated r31 operation: - check-place %0.#1 : Array [8495, 8507) - %63 = call silk/logger.emptyIndexes() : Array [8508, 8523) - %64 = read-place consume %0.#1 : Array [8476, 8524) - write-place %0.#1 <- %63 : Array replacement=Copy commit=AfterCleanup [8476, 8524) - %65 = move %64 [8456, 8524) - forward r32 [8456, 8524) generated + check-place %0.#1 : Array [8448, 8460) + %63 = call silk/logger.emptyIndexes() : Array [8461, 8476) + %64 = read-place consume %0.#1 : Array [8429, 8477) + write-place %0.#1 <- %63 : Array replacement=Copy commit=AfterCleanup [8429, 8477) + %65 = move %64 [8409, 8477) + forward r32 [8409, 8477) generated r32 operation: - check-place %0.#2 : Array [8563, 8575) - %66 = call silk/logger.emptyIndexes() : Array [8576, 8591) - %67 = read-place consume %0.#2 : Array [8544, 8592) - write-place %0.#2 <- %66 : Array replacement=Copy commit=AfterCleanup [8544, 8592) - %68 = move %67 [8524, 8592) - forward r33 [8524, 8592) generated + check-place %0.#2 : Array [8516, 8528) + %66 = call silk/logger.emptyIndexes() : Array [8529, 8544) + %67 = read-place consume %0.#2 : Array [8497, 8545) + write-place %0.#2 <- %66 : Array replacement=Copy commit=AfterCleanup [8497, 8545) + %68 = move %67 [8477, 8545) + forward r33 [8477, 8545) generated r33 operation: - %69 = read-place %0.#4 : usize [8602, 8612) - check-place %62[%69/8] : silk/logger.LogLevel [8592, 8613) - write-place %62[%69/8] <- %1 : silk/logger.LogLevel replacement=Copy commit=AfterCleanup [8592, 8621) - forward r34 [8592, 8621) generated + %69 = read-place %0.#4 : usize [8555, 8565) + check-place %62[%69/8] : silk/logger.LogLevel [8545, 8566) + write-place %62[%69/8] <- %1 : silk/logger.LogLevel replacement=Copy commit=AfterCleanup [8545, 8574) + forward r34 [8545, 8574) generated r34 operation: - %70 = read-place %0.#4 : usize [8632, 8642) - check-place %65[%70/8] : usize [8621, 8643) - write-place %65[%70/8] <- %43 : usize replacement=Copy commit=AfterCleanup [8621, 8652) - forward r35 [8621, 8652) generated + %70 = read-place %0.#4 : usize [8585, 8595) + check-place %65[%70/8] : usize [8574, 8596) + write-place %65[%70/8] <- %43 : usize replacement=Copy commit=AfterCleanup [8574, 8605) + forward r35 [8574, 8605) generated r35 operation: - %71 = read-place %0.#4 : usize [8663, 8673) - check-place %68[%71/8] : usize [8652, 8674) - %72 = slice-length %4 : i32 [8676, 8690) - write-place %68[%71/8] <- %72 : usize replacement=Copy commit=AfterCleanup [8652, 8690) - forward r36 [8652, 8690) generated + %71 = read-place %0.#4 : usize [8616, 8626) + check-place %68[%71/8] : usize [8605, 8627) + %72 = slice-length %4 : i32 [8629, 8643) + write-place %68[%71/8] <- %72 : usize replacement=Copy commit=AfterCleanup [8605, 8643) + forward r36 [8605, 8643) generated r36 operation: - check-place %0.#3 : Array [8690, 8706) - write-place %0.#3 <- %46 : Array replacement=Copy commit=AfterCleanup [8690, 8722) - forward r37 [8690, 8722) generated + check-place %0.#3 : Array [8643, 8659) + write-place %0.#3 <- %46 : Array replacement=Copy commit=AfterCleanup [8643, 8675) + forward r37 [8643, 8675) generated r37 operation: - check-place %0.#0 : Array [8722, 8736) - write-place %0.#0 <- %62 : Array replacement=Copy commit=AfterCleanup [8722, 8750) - forward r38 [8722, 8750) generated + check-place %0.#0 : Array [8675, 8689) + write-place %0.#0 <- %62 : Array replacement=Copy commit=AfterCleanup [8675, 8703) + forward r38 [8675, 8703) generated r38 operation: - check-place %0.#1 : Array [8750, 8765) - write-place %0.#1 <- %65 : Array replacement=Copy commit=AfterCleanup [8750, 8780) - forward r39 [8750, 8780) generated + check-place %0.#1 : Array [8703, 8718) + write-place %0.#1 <- %65 : Array replacement=Copy commit=AfterCleanup [8703, 8733) + forward r39 [8703, 8733) generated r39 operation: - check-place %0.#2 : Array [8780, 8795) - write-place %0.#2 <- %68 : Array replacement=Copy commit=AfterCleanup [8780, 8810) - forward r40 [8780, 8810) generated + check-place %0.#2 : Array [8733, 8748) + write-place %0.#2 <- %68 : Array replacement=Copy commit=AfterCleanup [8733, 8763) + forward r40 [8733, 8763) generated r40 operation: - check-place %0.#4 : usize [8810, 8823) - %73 = read-place %0.#4 : usize [8825, 8836) - %74 = literal 0 : usize [8849, 8850) - %75 = literal 1 : usize [8851, 8853) - %76 = call silk/usize.add(%74, %75) : usize [8838, 8854) - %77 = add %73, %76 : usize [8825, 8854) - write-place %0.#4 <- %77 : usize replacement=Copy commit=AfterCleanup [8810, 8854) - forward r41 [8810, 8854) generated + check-place %0.#4 : usize [8763, 8776) + %73 = read-place %0.#4 : usize [8778, 8789) + %74 = literal 0 : usize [8802, 8803) + %75 = literal 1 : usize [8804, 8806) + %76 = call silk/usize.add(%74, %75) : usize [8791, 8807) + %77 = add %73, %76 : usize [8778, 8807) + write-place %0.#4 <- %77 : usize replacement=Copy commit=AfterCleanup [8763, 8807) + forward r41 [8763, 8807) generated r41 operation: - check-place %0.#5 : usize [8854, 8875) - %78 = read-place %0.#5 : usize [8877, 8896) - %79 = slice-length %4 : i32 [8898, 8912) - %80 = add %78, %79 : usize [8877, 8912) - write-place %0.#5 <- %80 : usize replacement=Copy commit=AfterCleanup [8854, 8912) - forward r42 [8854, 8912) generated + check-place %0.#5 : usize [8807, 8828) + %78 = read-place %0.#5 : usize [8830, 8849) + %79 = slice-length %4 : i32 [8851, 8865) + %80 = add %78, %79 : usize [8830, 8865) + write-place %0.#5 <- %80 : usize replacement=Copy commit=AfterCleanup [8807, 8865) + forward r42 [8807, 8865) generated r42 operation: - %81 = construct () { } [8921, 8924) - %82 = effect-outcome tag=0 %81 : once Effect<() ! silk/logger.LogError> [8921, 8924) - forward r43 [8921, 8924) generated + %81 = construct () { } [8874, 8877) + %82 = effect-outcome tag=0 %81 : once Effect<() ! silk/logger.LogError> [8874, 8877) + forward r43 [8874, 8877) generated r43 cleanup: - drop %50 [8230, 8264) generated - drop %43 [8124, 8159) generated - drop %6 [7816, 7846) generated - drop %4 [7776, 7816) generated - drop %0 [7689, 7717) generated - return %82 [8921, 8924) + drop %50 [8183, 8217) generated + drop %43 [8077, 8112) generated + drop %6 [7769, 7799) generated + drop %4 [7729, 7769) generated + drop %0 [7642, 7670) generated + return %82 [8874, 8877) r6 operation: - forward r7 [7896, 7979) generated + forward r7 [7849, 7932) generated fn silk/logger.reject$effect$-1 params=1 locals=3 -> Effect entry=r0 r0 operation: - %1 = construct silk/logger.LogError { #0: %0 } [3010, 3034) - %2 = effect-outcome tag=1 %1 : Effect [3003, 3034) - return %2 [3003, 3034) -fn silk/effect.logTrace$effect$-1$provided$20 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %1 = construct silk/logger.LogError { #0: %0 } [3022, 3046) + %2 = effect-outcome tag=1 %1 : Effect [3015, 3046) + return %2 [3015, 3046) +fn silk/effect.logTrace$effect$-1$provided$19 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [5310, 5324) - %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$26 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5299, 5334) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5299, 5334) - return %6 [5299, 5334) -fn silk/effect.logDebug$effect$-1$provided$21 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %2 = enum-member silk/logger.LogLevel.Trace discriminant=0 lane=u8 : silk/logger.LogLevel [5302, 5316) + %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5291, 5326) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5291, 5326) + return %6 [5291, 5326) +fn silk/effect.logDebug$effect$-1$provided$20 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = enum-member silk/logger.LogLevel.Debug discriminant=1 lane=u8 : silk/logger.LogLevel [5526, 5540) - %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$26 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5515, 5550) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5515, 5550) - return %6 [5515, 5550) -fn silk/effect.log$effect$-1$provided$22 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %2 = enum-member silk/logger.LogLevel.Debug discriminant=1 lane=u8 : silk/logger.LogLevel [5518, 5532) + %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5507, 5542) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5507, 5542) + return %6 [5507, 5542) +fn silk/effect.log$effect$-1$provided$21 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = enum-member silk/logger.LogLevel.Info discriminant=2 lane=u8 : silk/logger.LogLevel [4695, 4708) - %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$26 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [4684, 4718) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [4684, 4718) - return %6 [4684, 4718) -fn silk/effect.logInfo$effect$-1$provided$23 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %2 = enum-member silk/logger.LogLevel.Info discriminant=2 lane=u8 : silk/logger.LogLevel [4687, 4700) + %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [4676, 4710) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [4676, 4710) + return %6 [4676, 4710) +fn silk/effect.logInfo$effect$-1$provided$22 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = enum-member silk/logger.LogLevel.Info discriminant=2 lane=u8 : silk/logger.LogLevel [5740, 5753) - %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$26 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5729, 5763) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5729, 5763) - return %6 [5729, 5763) -fn silk/effect.logWarning$effect$-1$provided$24 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %2 = enum-member silk/logger.LogLevel.Info discriminant=2 lane=u8 : silk/logger.LogLevel [5732, 5745) + %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5721, 5755) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5721, 5755) + return %6 [5721, 5755) +fn silk/effect.logWarning$effect$-1$provided$23 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = enum-member silk/logger.LogLevel.Warning discriminant=3 lane=u8 : silk/logger.LogLevel [5959, 5975) - %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$26 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5948, 5985) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5948, 5985) - return %6 [5948, 5985) -fn silk/effect.logError$effect$-1$provided$25 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %2 = enum-member silk/logger.LogLevel.Warning discriminant=3 lane=u8 : silk/logger.LogLevel [5951, 5967) + %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [5940, 5977) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5940, 5977) + return %6 [5940, 5977) +fn silk/effect.logError$effect$-1$provided$24 params=2 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %2 = enum-member silk/logger.LogLevel.Error discriminant=4 lane=u8 : silk/logger.LogLevel [6177, 6191) - %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$26 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [6166, 6201) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [6166, 6201) - return %6 [6166, 6201) -fn silk/effect.logAt$effect$-1$provided$26 params=3 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 + %2 = enum-member silk/logger.LogLevel.Error discriminant=4 lane=u8 : silk/logger.LogLevel [6169, 6183) + %5 = run-static-effect runner=silk/effect.logAt$effect$-1$provided$25 captures=%2:copy,%0:copy arguments=%1 propagate=1->1 : () [6158, 6193) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [6158, 6193) + return %6 [6158, 6193) +fn silk/effect.logAt$effect$-1$provided$25 params=3 locals=7 -> Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> entry=r0 r0 operation: - %3 = make-effect silk/logger.record$effect$-1 captures=%2:take,%0:copy,%1:copy : once Effect<() ! silk/logger.LogError> [5086, 5118) - %5 = run-effect-value %3 runner=silk/logger.record$effect$-1 providers=none arguments=none propagate=1->1 : () [5082, 5118) - %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5082, 5118) - return %6 [5082, 5118) + %3 = make-effect silk/logger.record$effect$-1 captures=%2:take,%0:copy,%1:copy : once Effect<() ! silk/logger.LogError> [5078, 5110) + %5 = run-effect-value %3 runner=silk/logger.record$effect$-1 providers=none arguments=none propagate=1->1 : () [5074, 5110) + %6 = effect-outcome tag=0 %5 : Effect<() ! silk/logger.LogError ? &mut silk/logger.Logger> [5074, 5110) + return %6 [5074, 5110) diff --git a/packages/compiler/test/goldens/match.mir.txt b/packages/compiler/test/goldens/match.mir.txt index 90de679be..565fe4336 100644 --- a/packages/compiler/test/goldens/match.mir.txt +++ b/packages/compiler/test/goldens/match.mir.txt @@ -7,8 +7,8 @@ layout golden/program.Box size=4 align=4 repr=aggregate cleanup-hook=none tail-p layout golden/program.Token size=4 align=4 repr=aggregate cleanup-hook=none tail-padding=0 field 0 kind: i32 offset=0 size=4 align=4 padding=0 calling i32 lanes=1 i32[] -calling golden/program.Box lanes=1 i32[golden/program#1.0.golden/program#0.0] -calling golden/program.Token lanes=1 i32[golden/program#0.0] +calling golden/program.Box lanes=1 i32[struct:golden/program:1:0.struct:golden/program:0:0] +calling golden/program.Token lanes=1 i32[struct:golden/program:0:0] fn golden/program.main params=0 locals=9 -> i32 entry=r0 r0 operation: %0 = literal 41 : i32 [180, 183) diff --git a/packages/compiler/test/goldens/operator.mir.txt b/packages/compiler/test/goldens/operator.mir.txt index ea1b9c9a2..e5d9291b9 100644 --- a/packages/compiler/test/goldens/operator.mir.txt +++ b/packages/compiler/test/goldens/operator.mir.txt @@ -4,10 +4,10 @@ target aarch64-apple-darwin kind=Native pointer=8/8 endian=little layout i32 size=4 align=4 repr=signed-i32 callable-environment golden/operator.main@callable:declaration:golden/operator:main:site:0 mode=shared size=4 align=4 fields=capture0->p1:copy:value@0 view=code@0,env@8,size=16 calling i32 lanes=1 i32[] -usize-literal 18446744073709551615 bits=64 available [2041, 2049) -usize-literal 0 bits=64 available [2143, 2144) -usize-literal 0 bits=64 available [2359, 2360) -usize-literal 1 bits=64 available [2463, 2464) +usize-literal 18446744073709551615 bits=64 available [2053, 2061) +usize-literal 0 bits=64 available [2155, 2156) +usize-literal 0 bits=64 available [2371, 2372) +usize-literal 1 bits=64 available [2475, 2476) fn golden/operator.main params=0 locals=7 -> i32 entry=r0 r0 operation: %0 = literal 2 : i32 [52, 54) @@ -20,5 +20,5 @@ fn golden/operator.main params=0 locals=7 -> i32 entry=r0 return %6 [52, 76) fn silk/i32.add params=2 locals=3 -> i32 entry=r0 r0 operation: - %2 = add %0, %1 : i32 [7762, 7792) - return %2 [7762, 7792) + %2 = add %0, %1 : i32 [8026, 8056) + return %2 [8026, 8056) diff --git a/packages/compiler/test/support/corpus.ts b/packages/compiler/test/support/corpus.ts index 499aa106b..62d70ee21 100644 --- a/packages/compiler/test/support/corpus.ts +++ b/packages/compiler/test/support/corpus.ts @@ -1939,7 +1939,7 @@ import silk.i32 as i32 import silk.hash as Hash import silk.hash { HashKey, HashSeed, Word } import silk.hash_map { HashMap, bucketCount, contains, get, insert, length, make, remove } -import silk.option { Option } +import silk.option { Option, unwrapOr } effect fn build() -> i32 ! OutOfMemoryError { let mut allocator = Allocator.systemAllocatorProvider() From 0d0963140c3267ac526ad0c8246bb564b99ff976 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 20:33:38 -0300 Subject: [PATCH 28/42] test(compiler): allow scheduler contention --- packages/compiler/test/SchedulerFiber.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler/test/SchedulerFiber.test.ts b/packages/compiler/test/SchedulerFiber.test.ts index 7d6d7d3d6..9e4a47c68 100644 --- a/packages/compiler/test/SchedulerFiber.test.ts +++ b/packages/compiler/test/SchedulerFiber.test.ts @@ -917,7 +917,9 @@ it.effect( const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) assert.strictEqual((instance.exports.silk_main as () => number)(), 42) }), - { timeout: 120_000 }, + // This is the suite's largest scheduler program and can exceed two minutes while the compiler's + // parallel acceptance workers contend for CPU; focused runs remain substantially faster. + { timeout: 240_000 }, ) it.effect( From e0269dcae225b2b880aa91da632635379f56cd51 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 21:15:01 -0300 Subject: [PATCH 29/42] fix(compiler): preserve nominal union actor parity --- .../docs/content/language/stdlib/allocator.md | 2 +- apps/docs/content/language/stdlib/box.md | 2 +- apps/docs/content/language/stdlib/bytes.md | 2 +- .../content/language/stdlib/child-process.md | 2 +- apps/docs/content/language/stdlib/effect.md | 2 +- .../docs/content/language/stdlib/execution.md | 2 +- apps/docs/content/language/stdlib/fiber.md | 2 +- .../content/language/stdlib/filesystem.md | 2 +- apps/docs/content/language/stdlib/format.md | 2 +- apps/docs/content/language/stdlib/hash-map.md | 2 +- apps/docs/content/language/stdlib/hash-set.md | 2 +- apps/docs/content/language/stdlib/hash.md | 2 +- .../content/language/stdlib/host-input.md | 2 +- .../language/stdlib/insecure-random.md | 2 +- .../content/language/stdlib/insecure-seed.md | 2 +- apps/docs/content/language/stdlib/layout.md | 2 +- .../language/stdlib/local-scheduler.md | 2 +- apps/docs/content/language/stdlib/logger.md | 2 +- apps/docs/content/language/stdlib/metrics.md | 2 +- .../language/stdlib/monotonic-clock.md | 2 +- apps/docs/content/language/stdlib/numeric.md | 2 +- apps/docs/content/language/stdlib/option.md | 12 +++- apps/docs/content/language/stdlib/order.md | 2 +- .../language/stdlib/os-child-process.md | 2 +- .../content/language/stdlib/os-filesystem.md | 2 +- .../content/language/stdlib/os-host-input.md | 2 +- .../language/stdlib/os-monotonic-clock.md | 2 +- .../docs/content/language/stdlib/os-random.md | 2 +- .../language/stdlib/os-standard-input.md | 2 +- .../language/stdlib/os-system-clock.md | 2 +- apps/docs/content/language/stdlib/random.md | 2 +- .../content/language/stdlib/raw-buffer.md | 2 +- apps/docs/content/language/stdlib/result.md | 24 ++++++- .../docs/content/language/stdlib/scheduler.md | 2 +- apps/docs/content/language/stdlib/shared.md | 2 +- apps/docs/content/language/stdlib/slot.md | 2 +- .../content/language/stdlib/standard-input.md | 2 +- .../language/stdlib/standard-streams.md | 2 +- apps/docs/content/language/stdlib/string.md | 6 +- .../content/language/stdlib/system-clock.md | 2 +- .../content/language/stdlib/unicode-tables.md | 2 +- apps/docs/content/language/stdlib/unicode.md | 2 +- apps/docs/content/language/stdlib/vector.md | 2 +- .../content/reference/effect-contracts.md | 6 +- .../functions-callables-and-control-flow.md | 15 +++- .../reference/patterns-and-destructuring.md | 38 ++++++++++ .../content/reference/values-and-types.md | 8 +-- .../specs/bootstrap-silk-stdlib/spec.md | 2 +- packages/compiler/src/CallResolution.ts | 5 +- packages/compiler/src/Completion.ts | 9 ++- packages/compiler/src/Diagnostic.ts | 11 +++ packages/compiler/src/ExpressionAnalysis.ts | 24 ++++++- packages/compiler/src/SemanticOccurrence.ts | 4 ++ packages/compiler/src/Stdlib.generated.ts | 12 ++-- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/stdlib/silk/option.silk | 2 +- packages/compiler/stdlib/silk/result.silk | 6 +- packages/compiler/stdlib/silk/string.silk | 4 +- .../compiler/test/EditorIntelligence.test.ts | 33 +++++++++ .../test/StdlibNamespaceAcceptance.test.ts | 12 ++-- packages/compiler/test/StructValues.test.ts | 71 +++++++++++++++++++ 61 files changed, 306 insertions(+), 80 deletions(-) diff --git a/apps/docs/content/language/stdlib/allocator.md b/apps/docs/content/language/stdlib/allocator.md index b3760b103..0c1fd0bbd 100644 --- a/apps/docs/content/language/stdlib/allocator.md +++ b/apps/docs/content/language/stdlib/allocator.md @@ -48,7 +48,7 @@ pub fn main() -> i32 { } ``` -Import as `Allocator` with `import silk.allocator`. +Import as `Allocator` with `import silk.allocator { Allocator }`. Public declarations: 5. diff --git a/apps/docs/content/language/stdlib/box.md b/apps/docs/content/language/stdlib/box.md index de576ed68..f8a8bcbae 100644 --- a/apps/docs/content/language/stdlib/box.md +++ b/apps/docs/content/language/stdlib/box.md @@ -54,7 +54,7 @@ pub fn main() -> i32 { } ``` -Import as `Box` with `import silk.box`. +Import as `Box` with `import silk.box { Box }`. Public declarations: 7. diff --git a/apps/docs/content/language/stdlib/bytes.md b/apps/docs/content/language/stdlib/bytes.md index 6a62c9e6e..164095b61 100644 --- a/apps/docs/content/language/stdlib/bytes.md +++ b/apps/docs/content/language/stdlib/bytes.md @@ -54,7 +54,7 @@ pub fn main() -> i32 { } ``` -Import as `Bytes` with `import silk.bytes`. +Import as `Bytes` with `import silk.bytes { Bytes }`. Public declarations: 8. diff --git a/apps/docs/content/language/stdlib/child-process.md b/apps/docs/content/language/stdlib/child-process.md index 33123c7e3..48516bade 100644 --- a/apps/docs/content/language/stdlib/child-process.md +++ b/apps/docs/content/language/stdlib/child-process.md @@ -81,7 +81,7 @@ pub fn main() -> i32 { } ``` -Import as `ChildProcess` with `import silk.child_process`. +Import as `ChildProcess` with `import silk.child_process { ChildProcess }`. Public declarations: 41. diff --git a/apps/docs/content/language/stdlib/effect.md b/apps/docs/content/language/stdlib/effect.md index b4b01eae8..0e6f46a3a 100644 --- a/apps/docs/content/language/stdlib/effect.md +++ b/apps/docs/content/language/stdlib/effect.md @@ -127,7 +127,7 @@ pub fn main() -> i32 { } ``` -Import as `Effect` with `import silk.effect`. +Import as `Effect` with `import silk.effect { Effect }`. Public declarations: 32. diff --git a/apps/docs/content/language/stdlib/execution.md b/apps/docs/content/language/stdlib/execution.md index cc328697e..2f2f00e88 100644 --- a/apps/docs/content/language/stdlib/execution.md +++ b/apps/docs/content/language/stdlib/execution.md @@ -25,7 +25,7 @@ Dropping a dormant Execution cancels it. A retained Wake keeps the complete iner until the Wake is consumed or dropped. [`drive`](#declaration-73696c6b2f657865637574696f6e3a3a6472697665) returns continued ownership only to `onSuspend`. -Import as `Execution` with `import silk.execution`. +Import as `Execution` with `import silk.execution { Execution }`. Public declarations: 5. diff --git a/apps/docs/content/language/stdlib/fiber.md b/apps/docs/content/language/stdlib/fiber.md index e81e308b7..d75593aef 100644 --- a/apps/docs/content/language/stdlib/fiber.md +++ b/apps/docs/content/language/stdlib/fiber.md @@ -20,7 +20,7 @@ Completion, cancellation, and cooperative yielding notify an Execution without a Dropping a Fiber stops observation. It does not cancel the task that produces the result. -Import as `Fiber` with `import silk.fiber`. +Import as `Fiber` with `import silk.fiber { Fiber }`. Public declarations: 16. diff --git a/apps/docs/content/language/stdlib/filesystem.md b/apps/docs/content/language/stdlib/filesystem.md index 43d745530..d4057fbf5 100644 --- a/apps/docs/content/language/stdlib/filesystem.md +++ b/apps/docs/content/language/stdlib/filesystem.md @@ -58,7 +58,7 @@ pub fn main() -> i32 { } ``` -Import as `FileSystem` with `import silk.filesystem`. +Import as `FileSystem` with `import silk.filesystem { FileSystem }`. Public declarations: 58. diff --git a/apps/docs/content/language/stdlib/format.md b/apps/docs/content/language/stdlib/format.md index 3ac626231..65149a749 100644 --- a/apps/docs/content/language/stdlib/format.md +++ b/apps/docs/content/language/stdlib/format.md @@ -59,7 +59,7 @@ pub fn main() -> i32 { } ``` -Import as `Format` with `import silk.format`. +Import as `Format` with `import silk.format { Format }`. Public declarations: 18. diff --git a/apps/docs/content/language/stdlib/hash-map.md b/apps/docs/content/language/stdlib/hash-map.md index 8f0867fc6..ccc812388 100644 --- a/apps/docs/content/language/stdlib/hash-map.md +++ b/apps/docs/content/language/stdlib/hash-map.md @@ -71,7 +71,7 @@ pub fn main() -> i32 { } ``` -Import as `HashMap` with `import silk.hash_map`. +Import as `HashMap` with `import silk.hash_map { HashMap }`. Public declarations: 16. diff --git a/apps/docs/content/language/stdlib/hash-set.md b/apps/docs/content/language/stdlib/hash-set.md index 56af32ace..a1b963d34 100644 --- a/apps/docs/content/language/stdlib/hash-set.md +++ b/apps/docs/content/language/stdlib/hash-set.md @@ -68,7 +68,7 @@ pub fn main() -> i32 { } ``` -Import as `HashSet` with `import silk.hash_set`. +Import as `HashSet` with `import silk.hash_set { HashSet }`. Public declarations: 13. diff --git a/apps/docs/content/language/stdlib/hash.md b/apps/docs/content/language/stdlib/hash.md index 165431293..95a7202fb 100644 --- a/apps/docs/content/language/stdlib/hash.md +++ b/apps/docs/content/language/stdlib/hash.md @@ -44,7 +44,7 @@ pub fn main() -> i32 { } ``` -Import as `HashKey` with `import silk.hash`. +Import as `HashKey` with `import silk.hash { HashKey }`. Public declarations: 8. diff --git a/apps/docs/content/language/stdlib/host-input.md b/apps/docs/content/language/stdlib/host-input.md index fc239d264..f06ec3039 100644 --- a/apps/docs/content/language/stdlib/host-input.md +++ b/apps/docs/content/language/stdlib/host-input.md @@ -89,7 +89,7 @@ pub fn main() -> i32 { } ``` -Import as `HostInput` with `import silk.host_input`. +Import as `HostInput` with `import silk.host_input { HostInput }`. Public declarations: 10. diff --git a/apps/docs/content/language/stdlib/insecure-random.md b/apps/docs/content/language/stdlib/insecure-random.md index baeb65ec6..117a67d9c 100644 --- a/apps/docs/content/language/stdlib/insecure-random.md +++ b/apps/docs/content/language/stdlib/insecure-random.md @@ -36,7 +36,7 @@ pub fn main() -> i32 { } ``` -Import as `InsecureRandom` with `import silk.insecure_random`. +Import as `InsecureRandom` with `import silk.insecure_random { InsecureRandom }`. Public declarations: 7. diff --git a/apps/docs/content/language/stdlib/insecure-seed.md b/apps/docs/content/language/stdlib/insecure-seed.md index 3c1947f7e..147b12622 100644 --- a/apps/docs/content/language/stdlib/insecure-seed.md +++ b/apps/docs/content/language/stdlib/insecure-seed.md @@ -36,7 +36,7 @@ pub fn main() -> i32 { } ``` -Import as `InsecureSeed` with `import silk.insecure_seed`. +Import as `InsecureSeed` with `import silk.insecure_seed { InsecureSeed }`. Public declarations: 8. diff --git a/apps/docs/content/language/stdlib/layout.md b/apps/docs/content/language/stdlib/layout.md index 337d72b90..d5d1873ad 100644 --- a/apps/docs/content/language/stdlib/layout.md +++ b/apps/docs/content/language/stdlib/layout.md @@ -33,7 +33,7 @@ pub fn main() -> i32 { } ``` -Import as `Layout` with `import silk.layout`. +Import as `Layout` with `import silk.layout { Layout }`. Public declarations: 6. diff --git a/apps/docs/content/language/stdlib/local-scheduler.md b/apps/docs/content/language/stdlib/local-scheduler.md index 591c86a34..3438bf8d3 100644 --- a/apps/docs/content/language/stdlib/local-scheduler.md +++ b/apps/docs/content/language/stdlib/local-scheduler.md @@ -14,7 +14,7 @@ Use [`execute`](#declaration-73696c6b2f6c6f63616c5f7363686564756c65723a3a6578656 Each call creates fresh task storage and a FIFO ready queue. The root is task zero and uses the same `Execution<()>` storage as every child. [`execute`](#declaration-73696c6b2f6c6f63616c5f7363686564756c65723a3a65786563757465) returns only after the root terminates. -Import as `LocalScheduler` with `import silk.local_scheduler`. +Import as `LocalScheduler` with `import silk.local_scheduler { LocalScheduler }`. Public declarations: 4. diff --git a/apps/docs/content/language/stdlib/logger.md b/apps/docs/content/language/stdlib/logger.md index d8a4e3e91..4d1343cba 100644 --- a/apps/docs/content/language/stdlib/logger.md +++ b/apps/docs/content/language/stdlib/logger.md @@ -57,7 +57,7 @@ pub fn main() -> i32 { } ``` -Import as `Logger` with `import silk.logger`. +Import as `Logger` with `import silk.logger { Logger }`. Public declarations: 14. diff --git a/apps/docs/content/language/stdlib/metrics.md b/apps/docs/content/language/stdlib/metrics.md index e18483ff5..24859064b 100644 --- a/apps/docs/content/language/stdlib/metrics.md +++ b/apps/docs/content/language/stdlib/metrics.md @@ -53,7 +53,7 @@ pub fn main() -> i32 { } ``` -Import as `AllocationMetrics` with `import silk.metrics`. +Import as `AllocationMetrics` with `import silk.metrics { AllocationMetrics }`. Public declarations: 7. diff --git a/apps/docs/content/language/stdlib/monotonic-clock.md b/apps/docs/content/language/stdlib/monotonic-clock.md index d50b6d3b8..5c15a6afb 100644 --- a/apps/docs/content/language/stdlib/monotonic-clock.md +++ b/apps/docs/content/language/stdlib/monotonic-clock.md @@ -23,7 +23,7 @@ virtual providers may satisfy a wait by advancing their own timeline without sle invalid or unrepresentable provider result traps because this service has no typed failure channel. -Import as `MonotonicClock` with `import silk.monotonic_clock`. +Import as `MonotonicClock` with `import silk.monotonic_clock { MonotonicClock }`. Public declarations: 5. diff --git a/apps/docs/content/language/stdlib/numeric.md b/apps/docs/content/language/stdlib/numeric.md index d30685aca..098a1bee7 100644 --- a/apps/docs/content/language/stdlib/numeric.md +++ b/apps/docs/content/language/stdlib/numeric.md @@ -33,7 +33,7 @@ pub fn main() -> i32 { } ``` -Import as `Integer` with `import silk.numeric`. +Import as `Integer` with `import silk.numeric { Integer }`. Public declarations: 3. diff --git a/apps/docs/content/language/stdlib/option.md b/apps/docs/content/language/stdlib/option.md index 51632e7e2..ac9da7995 100644 --- a/apps/docs/content/language/stdlib/option.md +++ b/apps/docs/content/language/stdlib/option.md @@ -45,7 +45,7 @@ pub fn main() -> i32 { } ``` -Import as `Option` with `import silk.option`. +Import as `Option` with `import silk.option { Option }`. Public declarations: 6. @@ -84,6 +84,16 @@ Option.Some { value: T }: Option The present variant, carrying the available owned value. + + +#### Field `value` + +```silk +pub value: T +``` + +The value moved through present-only combinator branches. + ## `none` diff --git a/apps/docs/content/language/stdlib/order.md b/apps/docs/content/language/stdlib/order.md index d0316999d..246c2d4a8 100644 --- a/apps/docs/content/language/stdlib/order.md +++ b/apps/docs/content/language/stdlib/order.md @@ -44,7 +44,7 @@ pub fn main() -> i32 { } ``` -Import as `Order` with `import silk.order`. +Import as `Order` with `import silk.order { Order }`. Public declarations: 11. diff --git a/apps/docs/content/language/stdlib/os-child-process.md b/apps/docs/content/language/stdlib/os-child-process.md index 63c5eb9c9..ab82fccac 100644 --- a/apps/docs/content/language/stdlib/os-child-process.md +++ b/apps/docs/content/language/stdlib/os-child-process.md @@ -40,7 +40,7 @@ pub fn main() -> i32 { } ``` -Import as `OsChildProcess` with `import silk.os_child_process`. +Import as `OsChildProcess` with `import silk.os_child_process { OsChildProcess }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/os-filesystem.md b/apps/docs/content/language/stdlib/os-filesystem.md index c036b0432..da7ce0532 100644 --- a/apps/docs/content/language/stdlib/os-filesystem.md +++ b/apps/docs/content/language/stdlib/os-filesystem.md @@ -60,7 +60,7 @@ pub fn main() -> i32 { } ``` -Import as `OsFileSystem` with `import silk.os_filesystem`. +Import as `OsFileSystem` with `import silk.os_filesystem { OsFileSystem }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/os-host-input.md b/apps/docs/content/language/stdlib/os-host-input.md index b353dc046..b78b8cc1d 100644 --- a/apps/docs/content/language/stdlib/os-host-input.md +++ b/apps/docs/content/language/stdlib/os-host-input.md @@ -40,7 +40,7 @@ pub fn main() -> i32 { } ``` -Import as `OsHostInput` with `import silk.os_host_input`. +Import as `OsHostInput` with `import silk.os_host_input { OsHostInput }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/os-monotonic-clock.md b/apps/docs/content/language/stdlib/os-monotonic-clock.md index b627d1b28..33e61bdeb 100644 --- a/apps/docs/content/language/stdlib/os-monotonic-clock.md +++ b/apps/docs/content/language/stdlib/os-monotonic-clock.md @@ -23,7 +23,7 @@ deadline overflow is a fatal trap. Linux requires `glibc` 2.17 or later and excl suspend time from this clock. macOS includes system suspend time. Direct WebAssembly has no ambient implementation. -Import as `OsMonotonicClock` with `import silk.os_monotonic_clock`. +Import as `OsMonotonicClock` with `import silk.os_monotonic_clock { OsMonotonicClock }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/os-random.md b/apps/docs/content/language/stdlib/os-random.md index 4a921c767..014aa3b7c 100644 --- a/apps/docs/content/language/stdlib/os-random.md +++ b/apps/docs/content/language/stdlib/os-random.md @@ -21,7 +21,7 @@ Host failure is a fatal trap because continuing with weak or partial bytes would service contract. GNU/Linux requires glibc 2.25 and Linux 3.17 or later. Direct WebAssembly and Windows do not provide this implementation. -Import as `OsRandom` with `import silk.os_random`. +Import as `OsRandom` with `import silk.os_random { OsRandom }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/os-standard-input.md b/apps/docs/content/language/stdlib/os-standard-input.md index 59b09da5a..3356062c1 100644 --- a/apps/docs/content/language/stdlib/os-standard-input.md +++ b/apps/docs/content/language/stdlib/os-standard-input.md @@ -39,7 +39,7 @@ pub fn main() -> i32 { } ``` -Import as `OsStandardInput` with `import silk.os_standard_input`. +Import as `OsStandardInput` with `import silk.os_standard_input { OsStandardInput }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/os-system-clock.md b/apps/docs/content/language/stdlib/os-system-clock.md index 976024d14..3ecd50de4 100644 --- a/apps/docs/content/language/stdlib/os-system-clock.md +++ b/apps/docs/content/language/stdlib/os-system-clock.md @@ -22,7 +22,7 @@ operations support the current Unix-family native targets and evaluator hosts. L `glibc` 2.17 or later and needs no `librt` link. Direct WebAssembly rejects reachable operations and does not add an ambient time import. -Import as `OsSystemClock` with `import silk.os_system_clock`. +Import as `OsSystemClock` with `import silk.os_system_clock { OsSystemClock }`. Public declarations: 2. diff --git a/apps/docs/content/language/stdlib/random.md b/apps/docs/content/language/stdlib/random.md index 0fdd0af8f..7580a634a 100644 --- a/apps/docs/content/language/stdlib/random.md +++ b/apps/docs/content/language/stdlib/random.md @@ -34,7 +34,7 @@ effect fn tokenWord() -> u64 } ``` -Import as `Random` with `import silk.random`. +Import as `Random` with `import silk.random { Random }`. Public declarations: 5. diff --git a/apps/docs/content/language/stdlib/raw-buffer.md b/apps/docs/content/language/stdlib/raw-buffer.md index 3e4581391..3be8e3717 100644 --- a/apps/docs/content/language/stdlib/raw-buffer.md +++ b/apps/docs/content/language/stdlib/raw-buffer.md @@ -61,7 +61,7 @@ pub fn main() -> i32 { } ``` -Import as `RawBuffer` with `import silk.raw_buffer`. +Import as `RawBuffer` with `import silk.raw_buffer { RawBuffer }`. Public declarations: 9. diff --git a/apps/docs/content/language/stdlib/result.md b/apps/docs/content/language/stdlib/result.md index a0a8f4ed4..2cae393ae 100644 --- a/apps/docs/content/language/stdlib/result.md +++ b/apps/docs/content/language/stdlib/result.md @@ -26,7 +26,7 @@ into a `Result` when a caller needs to inspect or store the outcome. ```silk import silk.result { Result } -fn half(value: i32) -> Result.Result { +fn half(value: i32) -> Result { if value == 0 { return Result.failResult(2) } @@ -45,7 +45,7 @@ pub fn main() -> i32 { } ``` -Import as `Result` with `import silk.result`. +Import as `Result` with `import silk.result { Result }`. Public declarations: 7. @@ -77,6 +77,16 @@ Result.Success { value: A }: Result A completed success. + + +#### Field `value` + +```silk +pub value: A +``` + +The produced success value. + ### `Failure` @@ -87,6 +97,16 @@ Result.Failure { error: F }: Result A completed failure. + + +#### Field `error` + +```silk +pub error: F +``` + +The produced failure value. + ## `succeed` diff --git a/apps/docs/content/language/stdlib/scheduler.md b/apps/docs/content/language/stdlib/scheduler.md index 920e771e3..a242a40de 100644 --- a/apps/docs/content/language/stdlib/scheduler.md +++ b/apps/docs/content/language/stdlib/scheduler.md @@ -20,7 +20,7 @@ Fiber. `Fiber.forkChild` registers that data after the exclusive service dispatc A provider must not expose the Fiber before publication succeeds. Publication failure consumes the complete pending value and returns no Fiber. -Import as `Scheduler` with `import silk.scheduler`. +Import as `Scheduler` with `import silk.scheduler { Scheduler }`. Public declarations: 17. diff --git a/apps/docs/content/language/stdlib/shared.md b/apps/docs/content/language/stdlib/shared.md index 49a4b9b64..16ffa1b2f 100644 --- a/apps/docs/content/language/stdlib/shared.md +++ b/apps/docs/content/language/stdlib/shared.md @@ -65,7 +65,7 @@ pub fn main() -> i32 { } ``` -Import as `Shared` with `import silk.shared`. +Import as `Shared` with `import silk.shared { Shared }`. Public declarations: 5. diff --git a/apps/docs/content/language/stdlib/slot.md b/apps/docs/content/language/stdlib/slot.md index 4399c96c3..49facad8d 100644 --- a/apps/docs/content/language/stdlib/slot.md +++ b/apps/docs/content/language/stdlib/slot.md @@ -62,7 +62,7 @@ pub fn main() -> i32 { } ``` -Import as `Slot` with `import silk.slot`. +Import as `Slot` with `import silk.slot { Slot }`. Public declarations: 5. diff --git a/apps/docs/content/language/stdlib/standard-input.md b/apps/docs/content/language/stdlib/standard-input.md index bf414e621..12b2c19ae 100644 --- a/apps/docs/content/language/stdlib/standard-input.md +++ b/apps/docs/content/language/stdlib/standard-input.md @@ -81,7 +81,7 @@ pub fn main() -> i32 { } ``` -Import as `StandardInput` with `import silk.standard_input`. +Import as `StandardInput` with `import silk.standard_input { StandardInput }`. Public declarations: 11. diff --git a/apps/docs/content/language/stdlib/standard-streams.md b/apps/docs/content/language/stdlib/standard-streams.md index 24569a1de..778453846 100644 --- a/apps/docs/content/language/stdlib/standard-streams.md +++ b/apps/docs/content/language/stdlib/standard-streams.md @@ -41,7 +41,7 @@ pub fn main() -> i32 { } ``` -Import as `StandardStreams` with `import silk.standard_streams`. +Import as `StandardStreams` with `import silk.standard_streams { StandardStreams }`. Public declarations: 7. diff --git a/apps/docs/content/language/stdlib/string.md b/apps/docs/content/language/stdlib/string.md index ebac807f3..828a573ba 100644 --- a/apps/docs/content/language/stdlib/string.md +++ b/apps/docs/content/language/stdlib/string.md @@ -33,9 +33,9 @@ import silk.usize pub fn main() -> i32 { let valid = String.fromUtf8(b"Silk") - |> unwrapOr("") + |> Result.unwrapOr("") let rejected = String.fromUtf8(b"a\x80") - |> unwrapOr("") + |> Result.unwrapOr("") let length = String.byteLength(valid) |> usize.toI32 let rejectedLength = String.byteLength(rejected) @@ -89,7 +89,7 @@ pub fn main() -> i32 { } ``` -Import as `String` with `import silk.string`. +Import as `String` with `import silk.string { String }`. Public declarations: 22. diff --git a/apps/docs/content/language/stdlib/system-clock.md b/apps/docs/content/language/stdlib/system-clock.md index 320f02647..1612ff8e5 100644 --- a/apps/docs/content/language/stdlib/system-clock.md +++ b/apps/docs/content/language/stdlib/system-clock.md @@ -58,7 +58,7 @@ pub fn main() -> i32 { } ``` -Import as `SystemClock` with `import silk.system_clock`. +Import as `SystemClock` with `import silk.system_clock { SystemClock }`. Public declarations: 7. diff --git a/apps/docs/content/language/stdlib/unicode-tables.md b/apps/docs/content/language/stdlib/unicode-tables.md index 366da2527..572649837 100644 --- a/apps/docs/content/language/stdlib/unicode-tables.md +++ b/apps/docs/content/language/stdlib/unicode-tables.md @@ -37,7 +37,7 @@ pub fn main() -> i32 { } ``` -Import as `UnicodeTables` with `import silk.unicode_tables`. +Import as `UnicodeTables` with `import silk.unicode_tables { UnicodeTables }`. Public declarations: 8. diff --git a/apps/docs/content/language/stdlib/unicode.md b/apps/docs/content/language/stdlib/unicode.md index 1eda634d1..47f750bd5 100644 --- a/apps/docs/content/language/stdlib/unicode.md +++ b/apps/docs/content/language/stdlib/unicode.md @@ -62,7 +62,7 @@ pub fn main() -> i32 { } ``` -Import as `Unicode` with `import silk.unicode`. +Import as `Unicode` with `import silk.unicode { Unicode }`. Public declarations: 6. diff --git a/apps/docs/content/language/stdlib/vector.md b/apps/docs/content/language/stdlib/vector.md index 33af02877..8bce2e5ac 100644 --- a/apps/docs/content/language/stdlib/vector.md +++ b/apps/docs/content/language/stdlib/vector.md @@ -105,7 +105,7 @@ pub fn main() -> i32 { } ``` -Import as `Vector` with `import silk.vector`. +Import as `Vector` with `import silk.vector { Vector }`. Public declarations: 20. diff --git a/apps/docs/content/reference/effect-contracts.md b/apps/docs/content/reference/effect-contracts.md index 180afcf17..e573b4845 100644 --- a/apps/docs/content/reference/effect-contracts.md +++ b/apps/docs/content/reference/effect-contracts.md @@ -349,8 +349,8 @@ remain for their own reference areas. value into `Result.Failure`. ```silk -import silk.effect as Effect -import silk.result as Result +import silk.effect { Effect } +import silk.result { Result } struct HttpError {} struct OutOfMemoryError {} @@ -359,7 +359,7 @@ effect fn fetch() -> i32 ! HttpError | OutOfMemoryError { fail HttpError {} } -effect fn inspect() -> Result.Result { +effect fn inspect() -> Result { return run Effect.result(fetch()) } ``` diff --git a/apps/docs/content/reference/functions-callables-and-control-flow.md b/apps/docs/content/reference/functions-callables-and-control-flow.md index 44b122fc4..109d4f889 100644 --- a/apps/docs/content/reference/functions-callables-and-control-flow.md +++ b/apps/docs/content/reference/functions-callables-and-control-flow.md @@ -642,12 +642,18 @@ such as `Status.Ready` covers that exact canonical member; a guarded occurrence Enum patterns bind no payload, and `_` covers every remaining member just as it does for a structural union. +A nominal union begins with one coverage leaf for each variant of its complete applied parent. +`Option.Some { value }` covers only `Option.Some`; a guarded occurrence removes nothing. +When the parent is itself a structural-union member, coverage retains the outer member and inner +variant path rather than flattening either identity. + **Boundary:** A match missing any member is invalid. A duplicate unguarded member, an arm after `_`, or another arm made impossible by earlier coverage is unreachable. A guarded arm alone never makes a member exhaustive. **Diagnostics:** An incomplete structural-union match reports `SEM0044` and lists the uncovered -members. An unreachable arm reports `SEM0043`. Scalar enums use the more specific coverage codes: +members or nominal variant paths. An unreachable arm reports `SEM0043`. Scalar enums use the more +specific coverage codes: `SEM0158` for missing members, `SEM0159` for a duplicate unguarded member, and `SEM0160` for an arm after `_`. A non-boolean guard reports `SEM0045`. Consuming a provisional guard binding reports `OWN0008` because later arms may still need the unchanged payload. @@ -683,12 +689,17 @@ A scalar enum member pattern selects one value but introduces no member subtype narrowing. The scrutinee and every use of it remain the enum's nominal type inside and outside the arm. +A nominal-union variant pattern narrows only the selected arm to its active payload fields. It does +not create a variant subtype: the complete applied parent remains the value type transported into +and out of the match. + **Boundary:** Match narrowing does not introduce general subtyping, mutate a binding's declared type, expose a union's numeric runtime tag, or carry a borrowed member binding outside its arm. **Diagnostics:** A structural pattern member absent from the scrutinee reports `SEM0042`. A scalar enum pattern from another enum reports `SEM0161`; an integer literal pattern against an enum reports -`SEM0162`. Using a member-only field without branch proof receives the ordinary field/type +`SEM0162`. An unknown nominal variant reports `SEM0167`, and a qualifier that is not a nominal union +reports `SEM0168`. Using a member-only field without branch proof receives the ordinary field/type diagnostic. Escaping a borrowed narrowed binding reports `OWN0006`. **Evidence:** [exhaustive matching specification](../../../../openspec/specs/bootstrap-exhaustive-matching/spec.md), diff --git a/apps/docs/content/reference/patterns-and-destructuring.md b/apps/docs/content/reference/patterns-and-destructuring.md index e69b17a95..bac0f802e 100644 --- a/apps/docs/content/reference/patterns-and-destructuring.md +++ b/apps/docs/content/reference/patterns-and-destructuring.md @@ -4,6 +4,8 @@ Patterns inspect existing value structure and introduce local bindings. Silk use grammar in exhaustive `match`, unconditional `let` destructuring, and conditional `if let`. The surrounding construct decides whether a pattern must always match or provides a mismatch path. Scalar enums additionally provide a qualified, payload-free member pattern for exhaustive `match`. +Nominal unions provide parent-qualified unit and named-field variant patterns in `match`, `let`, and +`if let` wherever the surrounding context admits a refutable selection. Patterns are not expressions. They perform no conversion, equality call, interface dispatch, constructor call, or user-defined extraction. @@ -532,3 +534,39 @@ member reports `SEM0161`, and an integer literal pattern against an enum reports **Evidence:** [scalar enum matching specification](../../../../openspec/specs/bootstrap-scalar-enums/spec.md), [enum matching tests](../../../../packages/compiler/test/ExhaustiveMatching.test.ts), [match coverage rules](functions-callables-and-control-flow.md#match-003--match-coverage-is-exhaustive-and-guards-do-not-prove-coverage). + +## PATT-021 — A qualified nominal-union variant selects one hierarchical leaf + +**Status:** Confirmed + +A nominal-union pattern names the complete applied parent followed by its variant. A unit variant +introduces no binding. A named-field variant uses the ordinary struct-field pattern rules, +including renaming, `..`, nesting, and access derived from the matched expression. + +```silk +union Option { Some { value: T }, None } + +fn unwrap(option: Option) -> i32 { + return match move option { + Option.Some { value } => value + Option.None => 0 + } +} +``` + +The selector is one leaf beneath `Option`, not a standalone type. If `Option` occurs in a +structural union, selection retains both the structural member and nominal variant identities. +Guards remain provisional, and exhausting the parent requires every declared variant even when a +payload contains `never`. + +**Boundary:** Pattern generic arguments are explicit rather than inferred from the scrutinee. A +variant cannot be used as a type, projected before selection, or flattened into the surrounding +structural union. A false guarded move leaves the whole active payload available to later arms. + +**Diagnostics:** An unknown variant reports `SEM0167`; a non-union qualifier reports `SEM0168`. +Missing, duplicate, unreachable, field, and ownership errors use the ordinary match, aggregate, and +ownership diagnostics while naming the complete hierarchical selection path. + +**Evidence:** [nominal-union pattern specification](../../../../openspec/changes/add-nominal-unions/specs/bootstrap-nominal-unions/spec.md), +[hierarchical matching tests](../../../../packages/compiler/test/StructValues.test.ts), +[match coverage rules](functions-callables-and-control-flow.md#match-003--match-coverage-is-exhaustive-and-guards-do-not-prove-coverage). diff --git a/apps/docs/content/reference/values-and-types.md b/apps/docs/content/reference/values-and-types.md index 830f556c4..56b77f29f 100644 --- a/apps/docs/content/reference/values-and-types.md +++ b/apps/docs/content/reference/values-and-types.md @@ -594,11 +594,11 @@ A nominal union is distinct from both other sum forms: independent generic parameters, explicit discriminants, or standalone type identity. Raw C unions and external linkage are outside this declaration form. -**Diagnostics:** An empty union reports `SEM0165`; duplicate variants report `SEM0166`; an empty -named-field variant reports `PAR0026`. Invalid variant fields preserve declaration facts for +**Diagnostics:** An empty union reports `SEM0164`; duplicate variants report `SEM0165`; an empty +named-field variant reports `SEM0166`. Invalid variant fields preserve declaration facts for tooling but make the complete parent unavailable for execution. -**Evidence:** [nominal union specification](../../../../openspec/changes/add-nominal-unions/specs/nominal-unions/spec.md), +**Evidence:** [nominal union specification](../../../../openspec/changes/add-nominal-unions/specs/bootstrap-nominal-unions/spec.md), [declaration tests](../../../../packages/compiler/test/DeclarationIndex.test.ts). ### NUNION-002 — Construction selects a qualified variant of one complete parent @@ -637,7 +637,7 @@ same-spelled field. Bind a selected variant before using its payload. Constructi variant subtype and never flattens the parent into `A | B`. **Evidence:** [constructor and visibility tests](../../../../packages/compiler/test/StructValues.test.ts), -[generic inference rules](../../../../openspec/changes/add-nominal-unions/specs/generic-inference/spec.md). +[generic inference rules](../../../../openspec/changes/add-nominal-unions/specs/bootstrap-type-generics/spec.md). ### NUNION-003 — Patterns select variants hierarchically and exhaustively diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md index 8a09be0d1..e369312f6 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-silk-stdlib/spec.md @@ -46,7 +46,7 @@ dual representations MUST NOT remain. #### Scenario: Migrate standard-library operations -- **WHEN** `map`, `mapError`, `flatMap`, predicates, `Effect.result`, and other Result producers or consumers are compiled +- **WHEN** `map`, `mapError`, `flatMap`, `unwrapOr`, `Effect.result`, and other Result producers or consumers are compiled - **THEN** they construct and match direct Result variants without a wrapper field or detached member types #### Scenario: Remove the Result wrapper representation diff --git a/packages/compiler/src/CallResolution.ts b/packages/compiler/src/CallResolution.ts index 9b5be3a4a..8e5801771 100644 --- a/packages/compiler/src/CallResolution.ts +++ b/packages/compiler/src/CallResolution.ts @@ -199,10 +199,11 @@ export function analyzeArguments( } } else if ( qualifier._tag === 'Resolved' && - qualifier.declaration._tag === 'StructDeclaration' && + (qualifier.declaration._tag === 'StructDeclaration' || + qualifier.declaration._tag === 'UnionDeclaration') && NameResolution.scopedModule(qualifier.declaration) !== undefined ) { - // A nominal type doubles as the scope of the module it names: `Vector.length(...)` names a + // A nominal aggregate doubles as the scope of the module it names: `Vector.length(...)` names a // public function of `silk/vector` because `Vector` matches that module's basename. The call // itself already resolves that way, but arguments are analyzed first, and without the same // lookup they get no expected types — which reads to a borrow argument as "no borrow is diff --git a/packages/compiler/src/Completion.ts b/packages/compiler/src/Completion.ts index 87cca5fac..4ccc00fa9 100644 --- a/packages/compiler/src/Completion.ts +++ b/packages/compiler/src/Completion.ts @@ -721,7 +721,8 @@ export const complete = (options: { replacement: replacement.span, candidates: stable(enumCandidates(lookup.declaration)), }) - if (lookup?._tag === 'Resolved' && lookup.declaration._tag === 'UnionDeclaration') + if (lookup?._tag === 'Resolved' && lookup.declaration._tag === 'UnionDeclaration') { + const scoped = NameResolution.scopedModule(lookup.declaration) return Object.freeze({ _tag: 'CompletionResult', context: Object.freeze({ @@ -732,8 +733,12 @@ export const complete = (options: { : (qualifier ?? 'union'), }), replacement: replacement.span, - candidates: stable(unionCandidates(lookup.declaration)), + candidates: stable([ + ...unionCandidates(lookup.declaration), + ...(scoped === undefined ? [] : namespaceCandidates(options.index, scoped)), + ]), }) + } if ( lookup?._tag === 'Resolved' && (lookup.declaration._tag === 'StructDeclaration' || diff --git a/packages/compiler/src/Diagnostic.ts b/packages/compiler/src/Diagnostic.ts index 8093e654b..bbcea05bd 100644 --- a/packages/compiler/src/Diagnostic.ts +++ b/packages/compiler/src/Diagnostic.ts @@ -3224,6 +3224,17 @@ export const missingPatternField = ( span, }) +export const inaccessiblePatternFields = (type: string, span: SourceSpan.SourceSpan): Diagnostic => + Object.freeze({ + _tag: 'Diagnostic', + phase: 'semantic', + code: missingPatternFieldCode, + severity: 'error', + message: `Pattern for ${type} must use .. to omit inaccessible fields`, + reason: Object.freeze({ _tag: 'MissingPatternField', type, field: '' }), + span, + }) + export const duplicatePatternField = ( field: string, originalSpan: SourceSpan.SourceSpan, diff --git a/packages/compiler/src/ExpressionAnalysis.ts b/packages/compiler/src/ExpressionAnalysis.ts index cbaeb02de..236adc87d 100644 --- a/packages/compiler/src/ExpressionAnalysis.ts +++ b/packages/compiler/src/ExpressionAnalysis.ts @@ -1860,6 +1860,7 @@ export const analyzePattern = ( ) } const label = nominal === undefined ? 'unknown aggregate' : Type.encode(nominal) + const outsideDefiningModule = nominal !== undefined && nominal.module !== source.id const seen = new Map() const bindings: Array = [] const fields = SyntaxTree.directNodes(node, 'PatternField').map((fieldNode): PatternFieldFact => { @@ -1875,7 +1876,20 @@ export const analyzePattern = ( : DeclarationFacts.lookupField(aggregateFields, name) let state: PatternFieldState = Object.freeze({ _tag: 'Unavailable' }) let resolvedField: DeclarationFacts.FieldFact | undefined - if (lookup?._tag === 'Resolved') { + if ( + lookup?._tag === 'Resolved' && + lookup.field.visibility === 'Private' && + outsideDefiningModule + ) { + const diagnostic = Diagnostic.inaccessibleProjectedField( + label, + name ?? '', + nameToken?.span ?? fieldNode.span, + ) + diagnostics.push(diagnostic) + counters.invalid = true + state = Object.freeze({ _tag: 'Unavailable', cause: Diagnostic.identity(diagnostic) }) + } else if (lookup?._tag === 'Resolved') { const original = seen.get(name ?? '') if (original === undefined) { resolvedField = lookup.field @@ -2006,10 +2020,17 @@ export const analyzePattern = ( (field) => field.nested?.omitted ?? [], ) if (aggregate !== undefined && !rest) { + let omittedInaccessible = false for (const field of aggregateFields) { if (field.name._tag !== 'Present' || seen.has(field.name.spelling)) continue + if (field.visibility === 'Private' && outsideDefiningModule) { + omittedInaccessible = true + continue + } diagnostics.push(Diagnostic.missingPatternField(label, field.name.spelling, node.span)) } + if (omittedInaccessible) + diagnostics.push(Diagnostic.inaccessiblePatternFields(label, node.span)) } else if (aggregate !== undefined && rest) { for (const field of aggregateFields) { if (field.name._tag === 'Present' && seen.has(field.name.spelling)) continue @@ -5975,6 +5996,7 @@ export function analyzeExpression( if ( qualifierLookup._tag === 'Resolved' && (qualifierLookup.declaration._tag === 'StructDeclaration' || + qualifierLookup.declaration._tag === 'UnionDeclaration' || qualifierLookup.declaration._tag === 'ServiceDeclaration' || qualifierLookup.declaration._tag === 'InterfaceDeclaration') && qualifierLookup.declaration.canonical._tag === 'Canonical' diff --git a/packages/compiler/src/SemanticOccurrence.ts b/packages/compiler/src/SemanticOccurrence.ts index 9e8f3e6fc..6cb78115e 100644 --- a/packages/compiler/src/SemanticOccurrence.ts +++ b/packages/compiler/src/SemanticOccurrence.ts @@ -214,10 +214,12 @@ const isNominalDeclaration = ( ): declaration is | DeclarationFacts.StructFact | DeclarationFacts.EnumFact + | DeclarationFacts.UnionFact | DeclarationFacts.ServiceFact | DeclarationFacts.InterfaceFact => declaration._tag === 'StructDeclaration' || declaration._tag === 'EnumDeclaration' || + declaration._tag === 'UnionDeclaration' || declaration._tag === 'ServiceDeclaration' || declaration._tag === 'InterfaceDeclaration' @@ -227,6 +229,7 @@ const declarationByNominal = ( ): | DeclarationFacts.StructFact | DeclarationFacts.EnumFact + | DeclarationFacts.UnionFact | DeclarationFacts.ServiceFact | DeclarationFacts.InterfaceFact | undefined => @@ -238,6 +241,7 @@ const declarationByNominal = ( ): declaration is | DeclarationFacts.StructFact | DeclarationFacts.EnumFact + | DeclarationFacts.UnionFact | DeclarationFacts.ServiceFact | DeclarationFacts.InterfaceFact => isNominalDeclaration(declaration) && diff --git a/packages/compiler/src/Stdlib.generated.ts b/packages/compiler/src/Stdlib.generated.ts index 6ca7c884b..4a20b1df3 100644 --- a/packages/compiler/src/Stdlib.generated.ts +++ b/packages/compiler/src/Stdlib.generated.ts @@ -792,14 +792,14 @@ export const modules = [ module: 'silk/option', path: 'silk/option.silk', sourceIdentity: 'silk/option', - digest: 'ade5e31b5032a6278b6f62bd5aee7a067b3f9e86d36731fa118f8b1d7b0e9e3d', + digest: '354e720f9a6aa40184a45864e037a157a610e97bbef2e453029b4d7f66af28bb', documentation: 'silk/option.silk', layer: 'portable', runtimeInventory: [], namespace: 'Option', aliases: ['None', 'Some'], source: - '//! Optional owned values that distinguish presence from absence without a failure channel.\n//!\n//! # When to use\n//! Use [`Option`] when absence is an expected answer and needs no error payload. Use [`map`] for a\n//! pure transform, [`flatMap`] when the transform may also return absence, and [`unwrapOr`] only\n//! when the caller is ready to consume the option.\n//!\n//! # Details\n//! `Option` is a nominal union with `Some` and `None` variants. Its combinators preserve affine\n//! ownership: a present value moves forward, while an unused fallback or abandoned branch drops.\n//!\n//! # Examples\n//! ## Transform and continue only when a value is present\n//! ```silk\n//! import silk.option { Option }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! fn positive(value: i32) -> Option.Option {\n//! if value > 0 {\n//! return Option.some(value)\n//! }\n//! return Option.none()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Option.some(21)\n//! let doubled = Option.map(move initial, double)\n//! let answer = Option.flatMap(move doubled, positive)\n//! let absent = Option.none()\n//! let missing = Option.map(move absent, double)\n//! let presentValue = Option.unwrapOr(move answer, 0)\n//! let absentValue = Option.unwrapOr(move missing, 0)\n//! return presentValue + absentValue\n//! }\n//! ```\n\n/// An owned value that is either [`Some`] or [`None`].\n///\n/// # Details\n///\n/// Match on an `Option` when both arms need custom behavior. Prefer [`map`], [`flatMap`], or\n/// [`unwrapOr`] for the common transform, continue, and default cases.\npub union Option {\n /// The absent variant; it carries no explanation for the absence.\n None,\n /// The present variant, carrying the available owned value.\n Some {\n /// The value moved through present-only combinator branches.\n value: T\n }\n}\n\n/// Constructs an absent optional value of the requested element type.\npub fn none() -> Option {\n return Option.None\n}\n\n/// Constructs a present option by moving `value` into it.\npub fn some(value: T) -> Option {\n return Option.Some { value: move value }\n}\n\n/// Applies `transform` once to a present value and keeps an absent value absent.\n///\n/// # Details\n///\n/// The callback is not called for [`None`]. This operation consumes `self`; use a shared borrow and\n/// `match` instead when the original option must remain available.\npub fn map(self: Option, transform: once fn(T) -> U) -> Option {\n return match move self {\n Option.Some { value } => some(transform(move value))\n Option.None => none()\n }\n}\n\n/// Continues a present value with a transform that itself answers with an Option, so the\n/// outcome stays one Option deep instead of nesting.\n///\n/// # Details\n///\n/// The callback runs once for [`Some`] and not at all for [`None`]. Use this when the next step may\n/// reject the value without needing to explain why; use a `Result` when rejection needs an error.\npub fn flatMap(self: Option, transform: once fn(T) -> Option) -> Option {\n return match move self {\n Option.Some { value } => transform(move value)\n Option.None => none()\n }\n}\n\n/// Returns the present value, or the fallback value when the option is absent.\n///\n/// # Details\n///\n/// Only the absent arm consumes the fallback. The present arm releases it, so exactly one of the\n/// two owned values leaves this call and the other drops.\n///\n/// # Examples\n///\n/// ## Choose between a present value and a fallback\n///\n/// ```silk\n/// import silk.option { Option }\n///\n/// pub fn main() -> i32 {\n/// let present = Option.some(7)\n/// let absent = Option.none()\n/// let first = move present\n/// |> Option.unwrapOr(0)\n/// let second = move absent\n/// |> Option.unwrapOr(5)\n/// return first + second\n/// }\n/// ```\npub fn unwrapOr(\n self: Option,\n /// The owned alternative consumed only when `self` is absent.\n fallback: T,\n) -> T {\n return match move self {\n Option.Some { value } => keepPresent(move value, move fallback)\n Option.None => move fallback\n }\n}\n\n/// Releases the fallback that a present value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepPresent(present: T, unused: T) -> T {\n drop unused\n return move present\n}\n', + '//! Optional owned values that distinguish presence from absence without a failure channel.\n//!\n//! # When to use\n//! Use [`Option`] when absence is an expected answer and needs no error payload. Use [`map`] for a\n//! pure transform, [`flatMap`] when the transform may also return absence, and [`unwrapOr`] only\n//! when the caller is ready to consume the option.\n//!\n//! # Details\n//! `Option` is a nominal union with `Some` and `None` variants. Its combinators preserve affine\n//! ownership: a present value moves forward, while an unused fallback or abandoned branch drops.\n//!\n//! # Examples\n//! ## Transform and continue only when a value is present\n//! ```silk\n//! import silk.option { Option }\n//!\n//! fn double(value: i32) -> i32 {\n//! return value * 2\n//! }\n//!\n//! fn positive(value: i32) -> Option.Option {\n//! if value > 0 {\n//! return Option.some(value)\n//! }\n//! return Option.none()\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Option.some(21)\n//! let doubled = Option.map(move initial, double)\n//! let answer = Option.flatMap(move doubled, positive)\n//! let absent = Option.none()\n//! let missing = Option.map(move absent, double)\n//! let presentValue = Option.unwrapOr(move answer, 0)\n//! let absentValue = Option.unwrapOr(move missing, 0)\n//! return presentValue + absentValue\n//! }\n//! ```\n\n/// An owned value that is either [`Some`] or [`None`].\n///\n/// # Details\n///\n/// Match on an `Option` when both arms need custom behavior. Prefer [`map`], [`flatMap`], or\n/// [`unwrapOr`] for the common transform, continue, and default cases.\npub union Option {\n /// The absent variant; it carries no explanation for the absence.\n None,\n /// The present variant, carrying the available owned value.\n Some {\n /// The value moved through present-only combinator branches.\n pub value: T\n }\n}\n\n/// Constructs an absent optional value of the requested element type.\npub fn none() -> Option {\n return Option.None\n}\n\n/// Constructs a present option by moving `value` into it.\npub fn some(value: T) -> Option {\n return Option.Some { value: move value }\n}\n\n/// Applies `transform` once to a present value and keeps an absent value absent.\n///\n/// # Details\n///\n/// The callback is not called for [`None`]. This operation consumes `self`; use a shared borrow and\n/// `match` instead when the original option must remain available.\npub fn map(self: Option, transform: once fn(T) -> U) -> Option {\n return match move self {\n Option.Some { value } => some(transform(move value))\n Option.None => none()\n }\n}\n\n/// Continues a present value with a transform that itself answers with an Option, so the\n/// outcome stays one Option deep instead of nesting.\n///\n/// # Details\n///\n/// The callback runs once for [`Some`] and not at all for [`None`]. Use this when the next step may\n/// reject the value without needing to explain why; use a `Result` when rejection needs an error.\npub fn flatMap(self: Option, transform: once fn(T) -> Option) -> Option {\n return match move self {\n Option.Some { value } => transform(move value)\n Option.None => none()\n }\n}\n\n/// Returns the present value, or the fallback value when the option is absent.\n///\n/// # Details\n///\n/// Only the absent arm consumes the fallback. The present arm releases it, so exactly one of the\n/// two owned values leaves this call and the other drops.\n///\n/// # Examples\n///\n/// ## Choose between a present value and a fallback\n///\n/// ```silk\n/// import silk.option { Option }\n///\n/// pub fn main() -> i32 {\n/// let present = Option.some(7)\n/// let absent = Option.none()\n/// let first = move present\n/// |> Option.unwrapOr(0)\n/// let second = move absent\n/// |> Option.unwrapOr(5)\n/// return first + second\n/// }\n/// ```\npub fn unwrapOr(\n self: Option,\n /// The owned alternative consumed only when `self` is absent.\n fallback: T,\n) -> T {\n return match move self {\n Option.Some { value } => keepPresent(move value, move fallback)\n Option.None => move fallback\n }\n}\n\n/// Releases the fallback that a present value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepPresent(present: T, unused: T) -> T {\n drop unused\n return move present\n}\n', }, { module: 'silk/order', @@ -974,13 +974,13 @@ export const modules = [ module: 'silk/result', path: 'silk/result.silk', sourceIdentity: 'silk/result', - digest: '09a5d29cc4fc832df199fa8f2e3ef54a8eac0fa60f39ea095291f3a4fd254b6a', + digest: 'f025bb4339f049ebfc4f6729279aa9714e31484c92deadfe130ace9071a747cd', documentation: 'silk/result.silk', layer: 'portable', runtimeInventory: [], namespace: 'Result', source: - '//! Completed success-or-failure values that can be inspected and transformed as ordinary data.\n//!\n//! # When to use\n//! Use [`Result`] after an effectful computation has been reified, or whenever both outcome arms\n//! belong in a value. Use [`map`] for success, [`mapError`] for failure, and [`flatMap`] for a\n//! success continuation that already returns a result.\n//!\n//! # Details\n//! `Result` owns either [`Success`] or [`Failure`]. Its combinators move the selected payload\n//! forward and preserve the other arm without inventing a runtime failure-row descriptor.\n//!\n//! Unlike an `Effect`, a `Result` is already completed ordinary data: it does not run,\n//! require a provider, or propagate through `fail`. Use `Effect.result` to turn one Effect execution\n//! into a `Result` when a caller needs to inspect or store the outcome.\n//!\n//! # Examples\n//! ## Transform a success and choose a fallback for failure\n//! ```silk\n//! import silk.result { Result }\n//!\n//! fn half(value: i32) -> Result.Result {\n//! if value == 0 {\n//! return Result.failResult(2)\n//! }\n//! return Result.succeed(value / 2)\n//! }\n//!\n//! fn addTwo(value: i32) -> i32 {\n//! return value + 2\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Result.succeed(80)\n//! let halved = Result.flatMap(move initial, half)\n//! let answer = Result.map(move halved, addTwo)\n//! return Result.unwrapOr(move answer, 0)\n//! }\n//! ```\n\n// Canonical completed typed outcome data. Failure rows project to ordinary value sums through\n// E directly; Result itself remains ordinary source-defined data with no runtime row descriptor.\n\nimport silk.bool as bool\n\n/// One completed outcome: either a success carrying `A` or a failure carrying `F`.\n///\n/// # Details\n///\n/// `Result` is the reified form of an Effect that has already run. Reifying an Effect turns its\n/// failure row into ordinary value data, which is what lets the failure combinators in\n/// `silk.effect` be written as ordinary Silk source instead of compiler built-ins.\n/// A `Result` is consumed when matched or passed to a transforming combinator. Use a borrowed\n/// match when the payload must remain available.\npub union Result {\n /// A completed success.\n Success {\n /// The produced success value.\n value: A\n },\n /// A completed failure.\n Failure {\n /// The produced failure value.\n error: F\n }\n}\n\n/// Constructs a completed success by moving `value` into the success arm.\npub fn succeed(value: A) -> Result {\n return Result.Success { value: move value }\n}\n\n/// Constructs a completed failure by moving `error` into the failure arm.\npub fn failResult(error: F) -> Result {\n return Result.Failure { error: move error }\n}\n\n/// Applies `transform` once to a success value and carries a failure through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Failure`]. This consumes the result and may change only its\n/// success type; use [`mapError`] to change the failure type instead.\npub fn map(self: Result, transform: once fn(A) -> B) -> Result {\n return match move self {\n Result.Success { value } => succeed(transform(move value))\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Applies `transform` once to a failure value and carries a success through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Success`]. This consumes the result and may change only its\n/// failure type.\npub fn mapError(self: Result, transform: once fn(F) -> G) -> Result {\n return match move self {\n Result.Success { value } => succeed(move value)\n Result.Failure { error } => failResult(transform(move error))\n }\n}\n\n/// Continues a success with a transform that answers with a Result of its own, so the outcome\n/// stays one Result deep instead of nesting.\n///\n/// # Details\n///\n/// A failure bypasses the callback unchanged. The callback must use the same failure type `F`, so\n/// use [`mapError`] before or after this operation when the steps use different error types.\npub fn flatMap(self: Result, transform: once fn(A) -> Result) -> Result {\n return match move self {\n Result.Success { value } => transform(move value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Returns the success value, or the fallback value when the outcome is a failure.\n///\n/// # Details\n///\n/// Only the failure arm consumes the fallback. The success arm releases it, and the failure arm\n/// releases the error, so exactly one owned value leaves this call and the other drops.\n/// Use `match` instead when the failure payload affects recovery or must be retained.\npub fn unwrapOr(\n self: Result,\n /// The owned alternative consumed only when `self` is a failure.\n fallback: A,\n) -> A {\n return match move self {\n Result.Success { value } => keepSuccess(move value, move fallback)\n Result.Failure { error } => keepFallback(move fallback, move error)\n }\n}\n\n/// Releases the fallback that a success value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepSuccess(success: A, unused: A) -> A {\n drop unused\n return move success\n}\n\n/// Releases the error the failure arm carried, and answers with the fallback it did need.\nfn keepFallback(fallback: A, error: F) -> A {\n drop error\n return move fallback\n}\n', + '//! Completed success-or-failure values that can be inspected and transformed as ordinary data.\n//!\n//! # When to use\n//! Use [`Result`] after an effectful computation has been reified, or whenever both outcome arms\n//! belong in a value. Use [`map`] for success, [`mapError`] for failure, and [`flatMap`] for a\n//! success continuation that already returns a result.\n//!\n//! # Details\n//! `Result` owns either [`Success`] or [`Failure`]. Its combinators move the selected payload\n//! forward and preserve the other arm without inventing a runtime failure-row descriptor.\n//!\n//! Unlike an `Effect`, a `Result` is already completed ordinary data: it does not run,\n//! require a provider, or propagate through `fail`. Use `Effect.result` to turn one Effect execution\n//! into a `Result` when a caller needs to inspect or store the outcome.\n//!\n//! # Examples\n//! ## Transform a success and choose a fallback for failure\n//! ```silk\n//! import silk.result { Result }\n//!\n//! fn half(value: i32) -> Result {\n//! if value == 0 {\n//! return Result.failResult(2)\n//! }\n//! return Result.succeed(value / 2)\n//! }\n//!\n//! fn addTwo(value: i32) -> i32 {\n//! return value + 2\n//! }\n//!\n//! pub fn main() -> i32 {\n//! let initial = Result.succeed(80)\n//! let halved = Result.flatMap(move initial, half)\n//! let answer = Result.map(move halved, addTwo)\n//! return Result.unwrapOr(move answer, 0)\n//! }\n//! ```\n\n// Canonical completed typed outcome data. Failure rows project to ordinary value sums through\n// E directly; Result itself remains ordinary source-defined data with no runtime row descriptor.\n\nimport silk.bool as bool\n\n/// One completed outcome: either a success carrying `A` or a failure carrying `F`.\n///\n/// # Details\n///\n/// `Result` is the reified form of an Effect that has already run. Reifying an Effect turns its\n/// failure row into ordinary value data, which is what lets the failure combinators in\n/// `silk.effect` be written as ordinary Silk source instead of compiler built-ins.\n/// A `Result` is consumed when matched or passed to a transforming combinator. Use a borrowed\n/// match when the payload must remain available.\npub union Result {\n /// A completed success.\n Success {\n /// The produced success value.\n pub value: A\n },\n /// A completed failure.\n Failure {\n /// The produced failure value.\n pub error: F\n }\n}\n\n/// Constructs a completed success by moving `value` into the success arm.\npub fn succeed(value: A) -> Result {\n return Result.Success { value: move value }\n}\n\n/// Constructs a completed failure by moving `error` into the failure arm.\npub fn failResult(error: F) -> Result {\n return Result.Failure { error: move error }\n}\n\n/// Applies `transform` once to a success value and carries a failure through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Failure`]. This consumes the result and may change only its\n/// success type; use [`mapError`] to change the failure type instead.\npub fn map(self: Result, transform: once fn(A) -> B) -> Result {\n return match move self {\n Result.Success { value } => succeed(transform(move value))\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Applies `transform` once to a failure value and carries a success through unchanged.\n///\n/// # Details\n///\n/// The callback is never called for [`Success`]. This consumes the result and may change only its\n/// failure type.\npub fn mapError(self: Result, transform: once fn(F) -> G) -> Result {\n return match move self {\n Result.Success { value } => succeed(move value)\n Result.Failure { error } => failResult(transform(move error))\n }\n}\n\n/// Continues a success with a transform that answers with a Result of its own, so the outcome\n/// stays one Result deep instead of nesting.\n///\n/// # Details\n///\n/// A failure bypasses the callback unchanged. The callback must use the same failure type `F`, so\n/// use [`mapError`] before or after this operation when the steps use different error types.\npub fn flatMap(self: Result, transform: once fn(A) -> Result) -> Result {\n return match move self {\n Result.Success { value } => transform(move value)\n Result.Failure { error } => failResult(move error)\n }\n}\n\n/// Returns the success value, or the fallback value when the outcome is a failure.\n///\n/// # Details\n///\n/// Only the failure arm consumes the fallback. The success arm releases it, and the failure arm\n/// releases the error, so exactly one owned value leaves this call and the other drops.\n/// Use `match` instead when the failure payload affects recovery or must be retained.\npub fn unwrapOr(\n self: Result,\n /// The owned alternative consumed only when `self` is a failure.\n fallback: A,\n) -> A {\n return match move self {\n Result.Success { value } => keepSuccess(move value, move fallback)\n Result.Failure { error } => keepFallback(move fallback, move error)\n }\n}\n\n/// Releases the fallback that a success value never needed. A match arm is one expression, so the\n/// arm that must both drop and produce delegates to this helper.\nfn keepSuccess(success: A, unused: A) -> A {\n drop unused\n return move success\n}\n\n/// Releases the error the failure arm carried, and answers with the fallback it did need.\nfn keepFallback(fallback: A, error: F) -> A {\n drop error\n return move fallback\n}\n', }, { module: 'silk/scheduler', @@ -1055,14 +1055,14 @@ export const modules = [ module: 'silk/string', path: 'silk/string.silk', sourceIdentity: 'silk/string', - digest: 'e921d7cea15edc465a3c5e6f3359f25ebe929294966cf6f881a4dd4af625f7d6', + digest: 'b09a3abd405af46fb39820915a441d6055256f8de05c58958ef2cbacdee76836', documentation: 'silk/string.silk', layer: 'portable', runtimeInventory: ['stringByteLength', 'stringFromUtf8Unchecked', 'stringUtf8Bytes'], namespace: 'String', aliases: ['InvalidUtf8', 'ScalarCursor', 'ScalarStep'], source: - '//! Valid UTF-8 text, including owned storage, byte validation, and scalar-by-scalar traversal.\n//!\n//! # When to use\n//! Use the built-in `string` type for borrowed text and [`String`] when text must own its storage.\n//! Use [`Bytes`] when arbitrary octets must survive without UTF-8 validation.\n//!\n//! # Details\n//! [`fromUtf8`] validates and borrows existing bytes without allocating; [`copyUtf8`] validates and\n//! owns a copy. [`append`] and [`appendOwned`] leave the original value unchanged if growth cannot\n//! allocate. Scalar cursors expose Unicode scalar values and byte offsets, not grapheme clusters.\n//!\n//! # Gotchas\n//! A [`ScalarCursor`] is meaningful only for the same unchanged string from which its traversal\n//! began. Start with [`scalarCursor`] and advance only with [`nextCursor`].\n//!\n//! # Examples\n//! ## Validate borrowed UTF-8 bytes\n//! ```silk\n//! import silk.result { Result }\n//!\n//! import silk.string { String }\n//!\n//! import silk.usize\n//!\n//! pub fn main() -> i32 {\n//! let valid = String.fromUtf8(b"Silk")\n//! |> unwrapOr("")\n//! let rejected = String.fromUtf8(b"a\\x80")\n//! |> unwrapOr("")\n//! let length = String.byteLength(valid)\n//! |> usize.toI32\n//! let rejectedLength = String.byteLength(rejected)\n//! |> usize.toI32\n//! return length + rejectedLength + 38\n//! }\n//! ```\n//!\n//! ## Build owned text and read its first scalar\n//! ```silk\n//! import silk.char\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option { Option }\n//!\n//! import silk.string { String }\n//!\n//! import silk.u32\n//!\n//! fn scalarCode(step: String.ScalarStep) -> i32 {\n//! return String.scalarValue(&step)\n//! |> char.toU32\n//! |> u32.toI32\n//! }\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let copying = String.copy("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let mut text = run copying\n//! let appending = String.append(&mut text, "!")\n//! |> Effect.provideMut(&mut allocator)\n//! let appended = run appending\n//! let stepped = String.nextScalar(String.view(&text), String.scalarCursor())\n//! let mapped = Option.map(move stepped, scalarCode)\n//! let scalar = Option.unwrapOr(move mapped, 0)\n//! return scalar - 191\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n copy as bytesCopy,\n append as bytesAppend,\n asSlice as bytesAsSlice,\n length as bytesLength\n}\nimport silk.char as char\nimport silk.char { fromU32 as charFromU32 }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { Option, none, some }\nimport silk.result { Result, failResult, succeed }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// An owned sequence of valid UTF-8 bytes that releases its storage on drop.\npub struct String {\n bytes: Bytes\n}\n\n/// The first byte offset at which UTF-8 validation failed.\npub struct InvalidUtf8 {\n /// The zero-based offset of the first byte that cannot continue a valid UTF-8 sequence.\n pub offset: usize\n}\n\n/// An opaque UTF-8 position used for scalar-by-scalar traversal.\npub struct ScalarCursor {\n byteOffset: usize\n}\n\n/// One decoded Unicode scalar, its byte offset, and the cursor after it.\npub struct ScalarStep {\n scalar: char\n byteOffset: usize\n next: ScalarCursor\n}\n\nfn byte(value: u8) -> u8 {\n return value\n}\n\nfn continuation(value: u8) -> bool {\n if value < byte(128) { return false }\n return value <= byte(191)\n}\n\n/// Returns the first byte offset at which UTF-8 validation fails, or None for complete valid text.\nfn firstInvalidUtf8(values: &[u8]) -> Option {\n let mut index = usize.ZERO\n while index < values.length {\n let first = values[index]\n if first < byte(128) {\n index = index + usize.ONE\n } else {\n if first < byte(194) { return some(index) }\n if first <= byte(223) {\n if values.length <= index + usize.ONE { return some(index) }\n if continuation(values[index + usize.ONE]) == false {\n return some(index + usize.ONE)\n }\n index = index + 2\n } else {\n if first <= byte(239) {\n if values.length <= index + 2 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if first == byte(224) {\n if second < byte(160) { return some(index + usize.ONE) }\n }\n if first == byte(237) {\n if byte(159) < second { return some(index + usize.ONE) }\n }\n index = index + 3\n } else {\n if byte(244) < first { return some(index) }\n if values.length <= index + 3 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n let fourth = values[index + 3]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if continuation(fourth) == false { return some(index + 3) }\n if first == byte(240) {\n if second < byte(144) { return some(index + usize.ONE) }\n }\n if first == byte(244) {\n if byte(143) < second { return some(index + usize.ONE) }\n }\n index = index + 4\n }\n }\n }\n }\n return none()\n}\n\n/// Borrows caller-validated UTF-8 bytes as text without runtime validation.\n///\n/// # When to use\n///\n/// Use this function only when an earlier operation proves that the complete byte view is UTF-8.\n/// Use [`fromUtf8`] when the bytes have not been validated.\n///\n/// # Gotchas\n///\n/// The caller must guarantee that the complete byte view is valid UTF-8 for the lifetime of the\n/// returned string view. Invalid bytes violate the safety contract.\npub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(values) }\n return ""\n}\n\n/// Validates a complete byte view and borrows it as text without allocating.\n///\n/// # Details\n///\n/// Success returns a `string` view with the same lexical lifetime as `values`. Failure returns the\n/// first invalid byte offset in [`InvalidUtf8`].\npub fn fromUtf8(values: &[u8]) -> Result {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let text = unsafe fromUtf8Unchecked(values)\n return succeed(text)\n return failResult(InvalidUtf8 { offset: usize.ZERO })\n}\n\n/// Constructs an empty owned String without allocating.\npub fn make() -> String {\n return String { bytes: bytesMake() }\n}\n\n/// Copies valid borrowed text into independently owned storage.\npub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = Intrinsic.stringUtf8Bytes(value)\n let bytes = run bytesCopy(source)\n return String { bytes: move bytes }\n}\n\n/// Validates complete UTF-8 bytes and copies them into independently owned storage.\n///\n/// # When to use\n///\n/// Use this function when the bytes must outlive their current buffer. Use [`fromUtf8`] for a\n/// borrowed result without allocation.\n///\n/// # Details\n///\n/// Invalid input returns [`InvalidUtf8`] as ordinary result data. Allocation failure remains in the\n/// Effect failure channel. No owned string is returned in either failure case.\npub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let bytes = run bytesCopy(values)\n return succeed(String { bytes: move bytes })\n}\n\n// Appending grows the existing storage rather than copying the whole string into fresh storage\n// first. The atomicity is the same either way — the underlying byte append builds its replacement\n// buffer in full before committing, so a failed allocation leaves the original untouched — but the\n// cost is not: composing a message from several pieces is what this API is for, and a copy per\n// piece made that quadratic in the message and linear in allocations.\n/// Appends complete valid text atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function for borrowed text. Use [`appendOwned`] when the suffix is an owned [`String`].\n///\n/// # Details\n///\n/// If growth fails, `self` keeps its prior contents and byte length.\npub effect fn append(self: &mut String, value: string) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = Intrinsic.stringUtf8Bytes(value)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Appends another owned String atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function to consume an owned suffix. Use [`append`] when the suffix is borrowed text.\n///\n/// # Details\n///\n/// This function consumes `value`. If growth fails, `self` keeps its prior contents and byte length.\npub effect fn appendOwned(self: &mut String, value: String) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = bytesAsSlice(&value.bytes)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Borrows the complete owned contents as valid text without allocating or copying.\npub fn view(self: &String) -> string {\n let bytes = bytesAsSlice(&self.bytes)\n return unsafe fromUtf8Unchecked(bytes)\n}\n\n/// Borrows a string\'s immutable UTF-8 encoding.\npub fn utf8Bytes(value: string) -> &[u8] {\n return Intrinsic.stringUtf8Bytes(value)\n}\n\n/// Returns a string\'s UTF-8 byte length.\npub fn byteLength(value: string) -> usize {\n return Intrinsic.stringByteLength(value)\n}\n\n/// Borrows an owned String\'s immutable UTF-8 encoding.\npub fn ownedUtf8Bytes(self: &String) -> &[u8] {\n return bytesAsSlice(&self.bytes)\n}\n\n/// Returns an owned String\'s initialized UTF-8 byte length.\npub fn ownedByteLength(self: &String) -> usize {\n return bytesLength(&self.bytes)\n}\n\n/// Creates a cursor at UTF-8 byte offset zero, before the first Unicode scalar.\npub fn scalarCursor() -> ScalarCursor {\n return ScalarCursor { byteOffset: usize.ZERO }\n}\n\n/// Returns a cursor\'s explicit UTF-8 byte offset.\npub fn cursorByteOffset(cursor: &ScalarCursor) -> usize {\n return cursor.byteOffset\n}\n\n/// Returns the decoded Unicode scalar value without consuming the step.\npub fn scalarValue(step: &ScalarStep) -> char {\n return step.scalar\n}\n\n/// Returns the UTF-8 byte offset at which one step begins.\npub fn scalarByteOffset(step: &ScalarStep) -> usize {\n return step.byteOffset\n}\n\n/// Consumes one scalar step and returns the cursor immediately after that scalar.\npub fn nextCursor(step: ScalarStep) -> ScalarCursor {\n return match move step {\n ScalarStep { scalar, byteOffset, next } => move next\n }\n}\n\nfn scalar32(value: u8) -> u32 {\n return u8.toU32(value)\n}\n\n/// Decodes the scalar at a cursor, or returns `None` at the end of the string.\n///\n/// # Details\n///\n/// A present step contains the scalar, its starting byte offset, and the next cursor. This function\n/// does not allocate.\n///\n/// # Gotchas\n///\n/// The cursor must come from [`scalarCursor`] or [`nextCursor`] for the same unchanged string.\npub fn nextScalar(value: string, cursor: ScalarCursor) -> Option {\n let bytes = Intrinsic.stringUtf8Bytes(value)\n let offset = cursor.byteOffset\n if offset == bytes.length { return none() }\n let first = bytes[offset]\n let mut scalar = scalar32(first)\n let mut width = usize.ONE\n if byte(194) <= first {\n if first <= byte(223) {\n scalar = (scalar32(first) - u32.toU32(192)) * u32.toU32(64)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128))\n width = 2\n } else {\n if first <= byte(239) {\n scalar = (scalar32(first) - u32.toU32(224)) * u32.toU32(4096)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128))\n width = 3\n } else {\n scalar = (scalar32(first) - u32.toU32(240)) * u32.toU32(262144)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(4096)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 3]) - u32.toU32(128))\n width = 4\n }\n }\n }\n return match move charFromU32(scalar) {\n Option.Some { value: decoded } => some(ScalarStep {\n scalar: decoded,\n byteOffset: offset,\n next: ScalarCursor { byteOffset: offset + width }\n })\n Option.None => none()\n }\n}\n', + '//! Valid UTF-8 text, including owned storage, byte validation, and scalar-by-scalar traversal.\n//!\n//! # When to use\n//! Use the built-in `string` type for borrowed text and [`String`] when text must own its storage.\n//! Use [`Bytes`] when arbitrary octets must survive without UTF-8 validation.\n//!\n//! # Details\n//! [`fromUtf8`] validates and borrows existing bytes without allocating; [`copyUtf8`] validates and\n//! owns a copy. [`append`] and [`appendOwned`] leave the original value unchanged if growth cannot\n//! allocate. Scalar cursors expose Unicode scalar values and byte offsets, not grapheme clusters.\n//!\n//! # Gotchas\n//! A [`ScalarCursor`] is meaningful only for the same unchanged string from which its traversal\n//! began. Start with [`scalarCursor`] and advance only with [`nextCursor`].\n//!\n//! # Examples\n//! ## Validate borrowed UTF-8 bytes\n//! ```silk\n//! import silk.result { Result }\n//!\n//! import silk.string { String }\n//!\n//! import silk.usize\n//!\n//! pub fn main() -> i32 {\n//! let valid = String.fromUtf8(b"Silk")\n//! |> Result.unwrapOr("")\n//! let rejected = String.fromUtf8(b"a\\x80")\n//! |> Result.unwrapOr("")\n//! let length = String.byteLength(valid)\n//! |> usize.toI32\n//! let rejectedLength = String.byteLength(rejected)\n//! |> usize.toI32\n//! return length + rejectedLength + 38\n//! }\n//! ```\n//!\n//! ## Build owned text and read its first scalar\n//! ```silk\n//! import silk.char\n//!\n//! import silk.allocator { Allocator }\n//!\n//! import silk.effect { Effect }\n//!\n//! import silk.option { Option }\n//!\n//! import silk.string { String }\n//!\n//! import silk.u32\n//!\n//! fn scalarCode(step: String.ScalarStep) -> i32 {\n//! return String.scalarValue(&step)\n//! |> char.toU32\n//! |> u32.toI32\n//! }\n//!\n//! effect fn build() -> i32\n//! ! Allocator.OutOfMemoryError {\n//! let mut allocator = Allocator.systemAllocatorProvider()\n//! let copying = String.copy("é")\n//! |> Effect.provideMut(&mut allocator)\n//! let mut text = run copying\n//! let appending = String.append(&mut text, "!")\n//! |> Effect.provideMut(&mut allocator)\n//! let appended = run appending\n//! let stepped = String.nextScalar(String.view(&text), String.scalarCursor())\n//! let mapped = Option.map(move stepped, scalarCode)\n//! let scalar = Option.unwrapOr(move mapped, 0)\n//! return scalar - 191\n//! }\n//!\n//! effect fn recover(error: Allocator.OutOfMemoryError) -> i32 {\n//! return 0\n//! }\n//!\n//! pub fn main() -> i32 {\n//! return run Effect.catchAll(build(), recover)\n//! }\n//! ```\n\nimport silk.bool as bool\nimport silk.bytes {\n Bytes,\n make as bytesMake,\n copy as bytesCopy,\n append as bytesAppend,\n asSlice as bytesAsSlice,\n length as bytesLength\n}\nimport silk.char as char\nimport silk.char { fromU32 as charFromU32 }\nimport silk.allocator { Allocator }\nimport silk.allocator { OutOfMemoryError }\nimport silk.option { Option, none, some }\nimport silk.result { Result, failResult, succeed }\nimport silk.u32 as u32\nimport silk.u8 as u8\nimport silk.usize as usize\n\n/// An owned sequence of valid UTF-8 bytes that releases its storage on drop.\npub struct String {\n bytes: Bytes\n}\n\n/// The first byte offset at which UTF-8 validation failed.\npub struct InvalidUtf8 {\n /// The zero-based offset of the first byte that cannot continue a valid UTF-8 sequence.\n pub offset: usize\n}\n\n/// An opaque UTF-8 position used for scalar-by-scalar traversal.\npub struct ScalarCursor {\n byteOffset: usize\n}\n\n/// One decoded Unicode scalar, its byte offset, and the cursor after it.\npub struct ScalarStep {\n scalar: char\n byteOffset: usize\n next: ScalarCursor\n}\n\nfn byte(value: u8) -> u8 {\n return value\n}\n\nfn continuation(value: u8) -> bool {\n if value < byte(128) { return false }\n return value <= byte(191)\n}\n\n/// Returns the first byte offset at which UTF-8 validation fails, or None for complete valid text.\nfn firstInvalidUtf8(values: &[u8]) -> Option {\n let mut index = usize.ZERO\n while index < values.length {\n let first = values[index]\n if first < byte(128) {\n index = index + usize.ONE\n } else {\n if first < byte(194) { return some(index) }\n if first <= byte(223) {\n if values.length <= index + usize.ONE { return some(index) }\n if continuation(values[index + usize.ONE]) == false {\n return some(index + usize.ONE)\n }\n index = index + 2\n } else {\n if first <= byte(239) {\n if values.length <= index + 2 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if first == byte(224) {\n if second < byte(160) { return some(index + usize.ONE) }\n }\n if first == byte(237) {\n if byte(159) < second { return some(index + usize.ONE) }\n }\n index = index + 3\n } else {\n if byte(244) < first { return some(index) }\n if values.length <= index + 3 { return some(index) }\n let second = values[index + usize.ONE]\n let third = values[index + 2]\n let fourth = values[index + 3]\n if continuation(second) == false { return some(index + usize.ONE) }\n if continuation(third) == false { return some(index + 2) }\n if continuation(fourth) == false { return some(index + 3) }\n if first == byte(240) {\n if second < byte(144) { return some(index + usize.ONE) }\n }\n if first == byte(244) {\n if byte(143) < second { return some(index + usize.ONE) }\n }\n index = index + 4\n }\n }\n }\n }\n return none()\n}\n\n/// Borrows caller-validated UTF-8 bytes as text without runtime validation.\n///\n/// # When to use\n///\n/// Use this function only when an earlier operation proves that the complete byte view is UTF-8.\n/// Use [`fromUtf8`] when the bytes have not been validated.\n///\n/// # Gotchas\n///\n/// The caller must guarantee that the complete byte view is valid UTF-8 for the lifetime of the\n/// returned string view. Invalid bytes violate the safety contract.\npub unsafe fn fromUtf8Unchecked(values: &[u8]) -> string {\n unsafe { return Intrinsic.stringFromUtf8Unchecked(values) }\n return ""\n}\n\n/// Validates a complete byte view and borrows it as text without allocating.\n///\n/// # Details\n///\n/// Success returns a `string` view with the same lexical lifetime as `values`. Failure returns the\n/// first invalid byte offset in [`InvalidUtf8`].\npub fn fromUtf8(values: &[u8]) -> Result {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let text = unsafe fromUtf8Unchecked(values)\n return succeed(text)\n return failResult(InvalidUtf8 { offset: usize.ZERO })\n}\n\n/// Constructs an empty owned String without allocating.\npub fn make() -> String {\n return String { bytes: bytesMake() }\n}\n\n/// Copies valid borrowed text into independently owned storage.\npub effect fn copy(value: string) -> String ! OutOfMemoryError ? &mut Allocator {\n let source = Intrinsic.stringUtf8Bytes(value)\n let bytes = run bytesCopy(source)\n return String { bytes: move bytes }\n}\n\n/// Validates complete UTF-8 bytes and copies them into independently owned storage.\n///\n/// # When to use\n///\n/// Use this function when the bytes must outlive their current buffer. Use [`fromUtf8`] for a\n/// borrowed result without allocation.\n///\n/// # Details\n///\n/// Invalid input returns [`InvalidUtf8`] as ordinary result data. Allocation failure remains in the\n/// Effect failure channel. No owned string is returned in either failure case.\npub effect fn copyUtf8(values: &[u8]) -> Result ! OutOfMemoryError ? &mut Allocator {\n let failure = match move firstInvalidUtf8(values) {\n Option.Some { value } => move value\n Option.None => values.length + usize.ONE\n }\n if failure <= values.length {\n return failResult(InvalidUtf8 { offset: failure })\n }\n let bytes = run bytesCopy(values)\n return succeed(String { bytes: move bytes })\n}\n\n// Appending grows the existing storage rather than copying the whole string into fresh storage\n// first. The atomicity is the same either way — the underlying byte append builds its replacement\n// buffer in full before committing, so a failed allocation leaves the original untouched — but the\n// cost is not: composing a message from several pieces is what this API is for, and a copy per\n// piece made that quadratic in the message and linear in allocations.\n/// Appends complete valid text atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function for borrowed text. Use [`appendOwned`] when the suffix is an owned [`String`].\n///\n/// # Details\n///\n/// If growth fails, `self` keeps its prior contents and byte length.\npub effect fn append(self: &mut String, value: string) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = Intrinsic.stringUtf8Bytes(value)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Appends another owned String atomically with respect to allocation failure.\n///\n/// # When to use\n///\n/// Use this function to consume an owned suffix. Use [`append`] when the suffix is borrowed text.\n///\n/// # Details\n///\n/// This function consumes `value`. If growth fails, `self` keeps its prior contents and byte length.\npub effect fn appendOwned(self: &mut String, value: String) -> () ! OutOfMemoryError ? &mut Allocator {\n let suffix = bytesAsSlice(&value.bytes)\n return run bytesAppend(&mut self.bytes, suffix)\n}\n\n/// Borrows the complete owned contents as valid text without allocating or copying.\npub fn view(self: &String) -> string {\n let bytes = bytesAsSlice(&self.bytes)\n return unsafe fromUtf8Unchecked(bytes)\n}\n\n/// Borrows a string\'s immutable UTF-8 encoding.\npub fn utf8Bytes(value: string) -> &[u8] {\n return Intrinsic.stringUtf8Bytes(value)\n}\n\n/// Returns a string\'s UTF-8 byte length.\npub fn byteLength(value: string) -> usize {\n return Intrinsic.stringByteLength(value)\n}\n\n/// Borrows an owned String\'s immutable UTF-8 encoding.\npub fn ownedUtf8Bytes(self: &String) -> &[u8] {\n return bytesAsSlice(&self.bytes)\n}\n\n/// Returns an owned String\'s initialized UTF-8 byte length.\npub fn ownedByteLength(self: &String) -> usize {\n return bytesLength(&self.bytes)\n}\n\n/// Creates a cursor at UTF-8 byte offset zero, before the first Unicode scalar.\npub fn scalarCursor() -> ScalarCursor {\n return ScalarCursor { byteOffset: usize.ZERO }\n}\n\n/// Returns a cursor\'s explicit UTF-8 byte offset.\npub fn cursorByteOffset(cursor: &ScalarCursor) -> usize {\n return cursor.byteOffset\n}\n\n/// Returns the decoded Unicode scalar value without consuming the step.\npub fn scalarValue(step: &ScalarStep) -> char {\n return step.scalar\n}\n\n/// Returns the UTF-8 byte offset at which one step begins.\npub fn scalarByteOffset(step: &ScalarStep) -> usize {\n return step.byteOffset\n}\n\n/// Consumes one scalar step and returns the cursor immediately after that scalar.\npub fn nextCursor(step: ScalarStep) -> ScalarCursor {\n return match move step {\n ScalarStep { scalar, byteOffset, next } => move next\n }\n}\n\nfn scalar32(value: u8) -> u32 {\n return u8.toU32(value)\n}\n\n/// Decodes the scalar at a cursor, or returns `None` at the end of the string.\n///\n/// # Details\n///\n/// A present step contains the scalar, its starting byte offset, and the next cursor. This function\n/// does not allocate.\n///\n/// # Gotchas\n///\n/// The cursor must come from [`scalarCursor`] or [`nextCursor`] for the same unchanged string.\npub fn nextScalar(value: string, cursor: ScalarCursor) -> Option {\n let bytes = Intrinsic.stringUtf8Bytes(value)\n let offset = cursor.byteOffset\n if offset == bytes.length { return none() }\n let first = bytes[offset]\n let mut scalar = scalar32(first)\n let mut width = usize.ONE\n if byte(194) <= first {\n if first <= byte(223) {\n scalar = (scalar32(first) - u32.toU32(192)) * u32.toU32(64)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128))\n width = 2\n } else {\n if first <= byte(239) {\n scalar = (scalar32(first) - u32.toU32(224)) * u32.toU32(4096)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128))\n width = 3\n } else {\n scalar = (scalar32(first) - u32.toU32(240)) * u32.toU32(262144)\n + (scalar32(bytes[offset + usize.ONE]) - u32.toU32(128)) * u32.toU32(4096)\n + (scalar32(bytes[offset + 2]) - u32.toU32(128)) * u32.toU32(64)\n + (scalar32(bytes[offset + 3]) - u32.toU32(128))\n width = 4\n }\n }\n }\n return match move charFromU32(scalar) {\n Option.Some { value: decoded } => some(ScalarStep {\n scalar: decoded,\n byteOffset: offset,\n next: ScalarCursor { byteOffset: offset + width }\n })\n Option.None => none()\n }\n}\n', }, { module: 'silk/system_clock', diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index c51e1073d..91129cdbe 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '1ccbffa0f7a2e25bcececfb9b25c1bc2b278a2ad47fcd3248fe583cf64dbf74e' +export const compilerDigest = '6e2aa6ae1e7a0d1801e574baec23d1290fc05395755aeff48fdc064964d05081' diff --git a/packages/compiler/stdlib/silk/option.silk b/packages/compiler/stdlib/silk/option.silk index 2e445066d..05683e3e4 100644 --- a/packages/compiler/stdlib/silk/option.silk +++ b/packages/compiler/stdlib/silk/option.silk @@ -49,7 +49,7 @@ pub union Option { /// The present variant, carrying the available owned value. Some { /// The value moved through present-only combinator branches. - value: T + pub value: T } } diff --git a/packages/compiler/stdlib/silk/result.silk b/packages/compiler/stdlib/silk/result.silk index 8ca2490f5..fbc18557c 100644 --- a/packages/compiler/stdlib/silk/result.silk +++ b/packages/compiler/stdlib/silk/result.silk @@ -18,7 +18,7 @@ //! ```silk //! import silk.result { Result } //! -//! fn half(value: i32) -> Result.Result { +//! fn half(value: i32) -> Result { //! if value == 0 { //! return Result.failResult(2) //! } @@ -55,12 +55,12 @@ pub union Result { /// A completed success. Success { /// The produced success value. - value: A + pub value: A }, /// A completed failure. Failure { /// The produced failure value. - error: F + pub error: F } } diff --git a/packages/compiler/stdlib/silk/string.silk b/packages/compiler/stdlib/silk/string.silk index 741c20ba4..6340f633f 100644 --- a/packages/compiler/stdlib/silk/string.silk +++ b/packages/compiler/stdlib/silk/string.silk @@ -24,9 +24,9 @@ //! //! pub fn main() -> i32 { //! let valid = String.fromUtf8(b"Silk") -//! |> unwrapOr("") +//! |> Result.unwrapOr("") //! let rejected = String.fromUtf8(b"a\x80") -//! |> unwrapOr("") +//! |> Result.unwrapOr("") //! let length = String.byteLength(valid) //! |> usize.toI32 //! let rejectedLength = String.byteLength(rejected) diff --git a/packages/compiler/test/EditorIntelligence.test.ts b/packages/compiler/test/EditorIntelligence.test.ts index b6d901f14..8ab75c14c 100644 --- a/packages/compiler/test/EditorIntelligence.test.ts +++ b/packages/compiler/test/EditorIntelligence.test.ts @@ -180,6 +180,39 @@ pub fn main() -> i32 { let state = State. return 0 }` ) }) +it.effect('completes variants and module operations from a file-named nominal union', () => { + const source = `import state { State } +pub fn main() -> i32 { let state = State. return 0 }` + return Analysis.makeRealized({ root: SourceFile.make('main', encoder.encode(source)) }).pipe( + Effect.provide( + SourceResolver.memory( + new Map([ + [ + 'state', + encoder.encode( + 'pub union State { Ready, Waiting { count: i32 } }\npub fn ready() -> State { return State.Ready }', + ), + ], + ]), + ), + ), + Effect.map((snapshot) => { + const offset = source.indexOf('State.') + 'State.'.length + const completion = Analysis.completionAt(snapshot, 'main', offset) + assert.deepEqual( + completion?.candidates.map((candidate) => [candidate.label, candidate.kind]), + [ + ['Ready', 'Constructor'], + ['Waiting', 'Constructor'], + ['ready', 'Function'], + ['State', 'Type'], + ], + ) + return undefined + }), + ) +}) + it.effect('navigates constructor and pattern variants through one canonical identity', () => { const source = `union Option { Some { value: T }, None } fn unwrap(option: Option) -> i32 { diff --git a/packages/compiler/test/StdlibNamespaceAcceptance.test.ts b/packages/compiler/test/StdlibNamespaceAcceptance.test.ts index 4b41495b8..8f19636eb 100644 --- a/packages/compiler/test/StdlibNamespaceAcceptance.test.ts +++ b/packages/compiler/test/StdlibNamespaceAcceptance.test.ts @@ -32,9 +32,9 @@ pub fn main() -> i32 { }` /** The selective import form keeps resolving the same members alongside the injected namespaces. */ -const selective = `import silk.vector { Vector, make } -import silk.option { Option, some } -import silk.result { Result, succeed } +const selective = `import silk.vector { Vector } +import silk.option { Option } +import silk.result { Result } fn settled(value: Result) -> i32 { return match move value { @@ -51,9 +51,9 @@ fn present(value: Option) -> i32 { } pub fn main() -> i32 { - let values = make() + let values = Vector.make() drop values - return present(some(40)) + settled(succeed(2)) + return present(Option.some(40)) + settled(Result.succeed(2)) }` const agrees = (name: string, source: string) => @@ -132,7 +132,7 @@ it.effect( ) it.effect( - 'keeps the selective import form compiling alongside the injected namespaces', + 'lets selectively imported nominal unions expose their module operations', () => agrees('stdlib-namespace/selective', selective), 60_000, ) diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 25fa77c8b..1824931dd 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -196,6 +196,77 @@ pub fn main() -> i32 { let secret = Secret.Open { value: 1, key: 2 } return 0 }` }), ) +it.effect('uses union variant field visibility as the external pattern boundary', () => + Effect.gen(function* () { + const model = ascii(`pub union Secret { Open { pub value: i32, key: i32 }, Closed } +pub fn make(value: i32) -> Secret { return Secret.Open { value: value, key: 7 } }`) + const valid = yield* multiSnapshot( + 'app/Main', + new Map([ + ['model/Secret', model], + [ + 'app/Main', + ascii(`import model.Secret { Secret, make } +fn reveal(secret: Secret) -> i32 { + return match move secret { + Secret.Open { value, .. } => value + Secret.Closed => 0 + } +} +pub fn main() -> i32 { return reveal(make(42)) }`), + ], + ]), + ) + assert.deepEqual(Analysis.diagnostics(valid), []) + const outcome = Analysis.evaluate(valid) + assert.strictEqual(outcome._tag, 'Completed') + if (outcome._tag === 'Completed') assert.strictEqual(outcome.result.value, 42n) + + const explicitPrivate = yield* multiSnapshot( + 'app/Main', + new Map([ + ['model/Secret', model], + [ + 'app/Main', + ascii(`import model.Secret { Secret, make } +pub fn main() -> i32 { + return match move make(42) { + Secret.Open { value, key } => value + Secret.Closed => 0 + } +}`), + ], + ]), + ) + assert.deepEqual( + Analysis.diagnostics(explicitPrivate).map((diagnostic) => diagnostic.code), + ['SEM0028'], + ) + + const undisclosedPrivate = yield* multiSnapshot( + 'app/Main', + new Map([ + ['model/Secret', model], + [ + 'app/Main', + ascii(`import model.Secret { Secret, make } +pub fn main() -> i32 { + return match move make(42) { + Secret.Open { value } => value + Secret.Closed => 0 + } +}`), + ], + ]), + ) + assert.deepEqual( + Analysis.diagnostics(undisclosedPrivate).map((diagnostic) => diagnostic.code), + ['SEM0046'], + ) + assert.notInclude(Analysis.diagnostics(undisclosedPrivate).at(0)?.message ?? '', 'key') + }), +) + it.effect('does not synthesize fields on the nominal union parent', () => Effect.gen(function* () { const self = yield* Analysis.ofSource( From 786f4bc226c13e65a5093e68e90577cf4fccbae3 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 21:29:44 -0300 Subject: [PATCH 30/42] fix(compiler): preserve union ownership semantics --- packages/compiler/src/BootstrapEvaluation.ts | 8 ++++ packages/compiler/src/CleanupPlan.ts | 20 ++++++++-- packages/compiler/src/ExecutableProperty.ts | 32 ++++++++++++--- packages/compiler/src/ExecutionAffinity.ts | 12 +++++- packages/compiler/src/LocalSharedOwnership.ts | 36 ++++++++++++++++- packages/compiler/src/LowerExpression.ts | 23 +++++++---- packages/compiler/src/LowerStatements.ts | 23 +++++++---- packages/compiler/src/Mir.ts | 1 + packages/compiler/src/MirEncoding.ts | 3 +- packages/compiler/src/MirLinearization.ts | 34 ++++++++++++++-- packages/compiler/src/MirVerification.ts | 4 ++ packages/compiler/src/NativeValueOperation.ts | 12 ++---- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 23 ++++++++++- packages/compiler/src/WasmMemory.ts | 10 +++++ packages/compiler/test/Analysis.test.ts | 29 ++++++++++++++ packages/compiler/test/StructValues.test.ts | 39 +++++++++++++++++++ packages/compiler/test/Suspendability.test.ts | 36 +++++++++++++++++ 18 files changed, 304 insertions(+), 43 deletions(-) diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index 2ae4e91c0..ad6baa0de 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -1977,6 +1977,14 @@ function* executeFunction( for (const cleanup of arm.selected.cleanup) { const owner = BootstrapStorage.selectFieldPath(payload, cleanup.path) const members = BootstrapStorage.cleanupMembers(cleanup.cleanup, owner) + write(cleanup.destination, { value: owner, fromCall: false }) + const released = yield* releaseThroughPlan( + cleanup.cleanup, + owner, + arm.provenance, + cleanup.destination.ordinal, + ) + if (released !== undefined) return released trace.push( Object.freeze({ _tag: 'MatchCleanup', diff --git a/packages/compiler/src/CleanupPlan.ts b/packages/compiler/src/CleanupPlan.ts index 433efd021..82a44f19c 100644 --- a/packages/compiler/src/CleanupPlan.ts +++ b/packages/compiler/src/CleanupPlan.ts @@ -384,15 +384,27 @@ export const cleanupTypeAtPath = ( module: current.module, name: current.name, }) - if (declaration?._tag !== 'StructDeclaration') return undefined + if ( + declaration === undefined || + (declaration._tag !== 'StructDeclaration' && declaration._tag !== 'UnionDeclaration') + ) + return undefined const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), current.arguments, ) if (substitution === undefined) return undefined - const field = declaration.fields.find((candidate) => - DeclarationFacts.sameFieldId(candidate.id, fieldId), - ) + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields + : declaration.variants.find( + (variant) => + fieldId.owner._tag === 'UnionVariantFieldOwnerId' && + variant.id.union.sourceId === fieldId.owner.variant.union.sourceId && + variant.id.union.ordinal === fieldId.owner.variant.union.ordinal && + variant.id.ordinal === fieldId.owner.variant.ordinal, + )?.fields + const field = fields?.find((candidate) => DeclarationFacts.sameFieldId(candidate.id, fieldId)) current = field?.declaredType._tag === 'Resolved' ? Type.substitute(field.declaredType.type, substitution) diff --git a/packages/compiler/src/ExecutableProperty.ts b/packages/compiler/src/ExecutableProperty.ts index 7bbcebe51..16c2361db 100644 --- a/packages/compiler/src/ExecutableProperty.ts +++ b/packages/compiler/src/ExecutableProperty.ts @@ -80,26 +80,38 @@ const nestedLoanCauses = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return [] + if ( + declaration === undefined || + (declaration._tag !== 'StructDeclaration' && declaration._tag !== 'UnionDeclaration') + ) + return [] const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), type.arguments, ) ?? new Map() const next = new Set(active).add(identity) - return declaration.fields.flatMap((field) => { + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields.map((field) => ({ field, owner: identity })) + : declaration.variants.flatMap((variant) => { + const variantName = + variant.name._tag === 'Present' ? variant.name.spelling : `#${variant.id.ordinal}` + return variant.fields.map((field) => ({ field, owner: `${identity}.${variantName}` })) + }) + return fields.flatMap(({ field, owner }) => { if (field.declaredType._tag === 'Resolved') { return nestedLoanCauses( index, Type.substitute(field.declaredType.type, substitution), [ ...path, - `${identity}.${field.name._tag === 'Present' ? field.name.spelling : `#${field.id.ordinal}`}`, + `${owner}.${field.name._tag === 'Present' ? field.name.spelling : `#${field.id.ordinal}`}`, ], next, ) } - return [cause('Unavailable', [...path, `${identity}.#${field.id.ordinal}`])] + return [cause('Unavailable', [...path, `${owner}.#${field.id.ordinal}`])] }) } @@ -230,14 +242,22 @@ const representedSubjectsOfType = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return [] + if ( + declaration === undefined || + (declaration._tag !== 'StructDeclaration' && declaration._tag !== 'UnionDeclaration') + ) + return [] const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), type.arguments, ) ?? new Map() const next = new Set(active).add(identity) - return declaration.fields.flatMap((field) => + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields + : declaration.variants.flatMap((variant) => variant.fields) + return fields.flatMap((field) => field.declaredType._tag === 'Resolved' ? representedSubjectsOfType( discovery, diff --git a/packages/compiler/src/ExecutionAffinity.ts b/packages/compiler/src/ExecutionAffinity.ts index ebf33446f..0c0eeac99 100644 --- a/packages/compiler/src/ExecutionAffinity.ts +++ b/packages/compiler/src/ExecutionAffinity.ts @@ -138,14 +138,22 @@ const ofTypeInner = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return unrestricted + if ( + declaration === undefined || + (declaration._tag !== 'StructDeclaration' && declaration._tag !== 'UnionDeclaration') + ) + return unrestricted const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), type.arguments, ) ?? new Map() const next = new Set(active).add(key) - const fields = declaration.fields.map((field): ExecutionAffinity => { + const declarationFields = + declaration._tag === 'StructDeclaration' + ? declaration.fields + : declaration.variants.flatMap((variant) => variant.fields) + const fields = declarationFields.map((field): ExecutionAffinity => { if (field.declaredType._tag !== 'Resolved') return unavailable(declaredCauses(field.declaredType)) return ofTypeInner(index, Type.substitute(field.declaredType.type, substitution), next) diff --git a/packages/compiler/src/LocalSharedOwnership.ts b/packages/compiler/src/LocalSharedOwnership.ts index f3eee930d..976ccf10b 100644 --- a/packages/compiler/src/LocalSharedOwnership.ts +++ b/packages/compiler/src/LocalSharedOwnership.ts @@ -57,6 +57,14 @@ export type ObligationPlan = readonly obligations: ObligationPlan }> } + | { + readonly _tag: 'ActiveNominalUnion' + readonly type: Type.Nominal + readonly cases: ReadonlyArray<{ + readonly variant: DeclarationFacts.UnionVariantId + readonly obligations: ObligationPlan + }> + } | { readonly _tag: 'Unavailable'; readonly causes: ReadonlyArray } export const none: ObligationPlan = Object.freeze({ _tag: 'NoLocalSharedObligation' }) @@ -131,13 +139,36 @@ const ofTypeInner = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') return none + if ( + declaration === undefined || + (declaration._tag !== 'StructDeclaration' && declaration._tag !== 'UnionDeclaration') + ) + return none const substitution = TypeInference.substitution( declaration.typeParameters.map((parameter) => parameter.type), type.arguments, ) ?? new Map() const next = new Set(active).add(key) + if (declaration._tag === 'UnionDeclaration') + return Object.freeze({ + _tag: 'ActiveNominalUnion', + type, + cases: Object.freeze( + declaration.variants.map((variant) => + Object.freeze({ + variant: variant.id, + obligations: product( + variant.fields.map((field) => + field.declaredType._tag === 'Resolved' + ? ofTypeInner(index, Type.substitute(field.declaredType.type, substitution), next) + : unavailable(causeOf(field.declaredType)), + ), + ), + }), + ), + ), + }) return product( declaration.fields.map((field) => field.declaredType._tag === 'Resolved' @@ -191,6 +222,7 @@ export const count = (self: ObligationPlan, activeUnionCase = 0): number => { case 'Repeat': return self.length * count(self.element) case 'ActiveUnion': + case 'ActiveNominalUnion': return count(self.cases.at(activeUnionCase)?.obligations ?? none) } } @@ -210,6 +242,8 @@ export const encode = (self: ObligationPlan): string => { return `repeat(${self.length},${encode(self.element)})` case 'ActiveUnion': return `active-union(${self.cases.map((entry) => `${Type.key(entry.member)}:${encode(entry.obligations)}`).join(',')})` + case 'ActiveNominalUnion': + return `active-nominal-union(${Type.key(self.type)};${self.cases.map((entry) => `variant#${entry.variant.ordinal}:${encode(entry.obligations)}`).join(',')})` case 'Unavailable': return `unavailable(${self.causes.map((cause) => `${cause.code}@${cause.span.sourceId}:${cause.span.start}:${cause.ordinal}`).join(',')})` } diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index 07e395308..dfe5fe3e1 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -1525,6 +1525,20 @@ export function lowerExpressionInner( const ownedArm = ownership?.arms.find( (candidate) => candidate.id.ordinal === arm.id.ordinal, ) + const cleanup: Array = [] + for (const release of ownedArm?.cleanup ?? []) { + const plan = specializedCleanup(fn, release.cleanup) + if (plan._tag === 'NoCleanup') continue + const type = fn.type(plan.type) + if (type === undefined) return undefined + cleanup.push( + Object.freeze({ + destination: fn.alloc(type), + path: release.path, + cleanup: plan, + }), + ) + } arms.push( Object.freeze({ id: arm.id, @@ -1538,14 +1552,7 @@ export function lowerExpressionInner( access: expression.access, operations: selectedOperations, result: selectedResult.result, - cleanup: Object.freeze( - (ownedArm?.cleanup ?? []).map((release) => - Object.freeze({ - path: release.path, - cleanup: specializedCleanup(fn, release.cleanup), - }), - ), - ), + cleanup: Object.freeze(cleanup), endBorrow: expression.access === 'Shared' || expression.access === 'Exclusive', }), provenance: authored(arm.span), diff --git a/packages/compiler/src/LowerStatements.ts b/packages/compiler/src/LowerStatements.ts index 5ab575ff0..cce74ee03 100644 --- a/packages/compiler/src/LowerStatements.ts +++ b/packages/compiler/src/LowerStatements.ts @@ -131,6 +131,20 @@ export const lowerPatternSelection = ( const ownedArm = ownership?.arms.find( (candidate) => candidate.id.ordinal === selection.arm.ordinal, ) + const cleanup: Array = [] + for (const release of ownedArm?.cleanup ?? []) { + const plan = specializedCleanup(fn, release.cleanup) + if (plan._tag === 'NoCleanup') continue + const type = fn.type(plan.type) + if (type === undefined) return undefined + cleanup.push( + Object.freeze({ + destination: fn.alloc(type), + path: release.path, + cleanup: plan, + }), + ) + } const selectedArm: Mir.MatchArm = Object.freeze({ id: selection.arm, ...(member === undefined ? {} : { member }), @@ -142,14 +156,7 @@ export const lowerPatternSelection = ( access: selection.access, operations: selectedOperations, result: selectedResult.result, - cleanup: Object.freeze( - (ownedArm?.cleanup ?? []).map((release) => - Object.freeze({ - path: release.path, - cleanup: specializedCleanup(fn, release.cleanup), - }), - ), - ), + cleanup: Object.freeze(cleanup), endBorrow: false, }), provenance: authored(selection.span), diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index 12401d725..3e67f2507 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -1191,6 +1191,7 @@ export interface MatchArm { readonly operations: ReadonlyArray readonly result: LocalId readonly cleanup: ReadonlyArray<{ + readonly destination: LocalId readonly path: ReadonlyArray readonly cleanup: CleanupPlan.CleanupPlan }> diff --git a/packages/compiler/src/MirEncoding.ts b/packages/compiler/src/MirEncoding.ts index 5b8e6d563..bb2ecb993 100644 --- a/packages/compiler/src/MirEncoding.ts +++ b/packages/compiler/src/MirEncoding.ts @@ -253,7 +253,8 @@ const operationLines = (operation: Operation, indent: string): ReadonlyArray operationLines(child, `${indent} `)), ...arm.selected.cleanup.map( - (entry) => `${indent} cleanup ${fieldPathText(entry.path)} ${entry.cleanup._tag}`, + (entry) => + `${indent} cleanup ${localText(entry.destination)} <- ${fieldPathText(entry.path)} ${entry.cleanup._tag}`, ), ] }), diff --git a/packages/compiler/src/MirLinearization.ts b/packages/compiler/src/MirLinearization.ts index 374e4932c..e6320e88a 100644 --- a/packages/compiler/src/MirLinearization.ts +++ b/packages/compiler/src/MirLinearization.ts @@ -1,4 +1,5 @@ import type { ControlProvenance } from './Backend.js' +import type * as DeclarationFacts from './DeclarationFacts.js' import type * as Layout from './Layout.js' import * as Match from './Match.js' import * as Mir from './Mir.js' @@ -72,7 +73,9 @@ export type LinearOperation = readonly scrutinee: Mir.LocalId readonly shape: Layout.CallingShape readonly member: Match.CoverageIdentity - readonly binding: Mir.MatchBinding + readonly destination: Mir.LocalId + readonly path: ReadonlyArray + readonly type: Mir.Type readonly provenance: Mir.Provenance } @@ -175,7 +178,7 @@ export const destinationOf = (operation: LinearOperation): Mir.LocalId | undefin case 'OsOpenOutcome': return operation.valid case 'BindMatch': - return operation.binding.destination + return operation.destination case 'CheckPlace': case 'WritePlace': case 'EndLoan': @@ -612,18 +615,43 @@ export const expandMatches = ( scrutinee: match.scrutinee, shape: match.scrutineeShape, member: bindingMember, - binding, + destination: binding.destination, + path: binding.path, + type: binding.type, provenance: binding.provenance, }), ), ) const selected = reserve() + const cleanup = arm.selected.cleanup.flatMap((entry): ReadonlyArray => { + const type = fn.localTypes.at(entry.destination.ordinal) + if (type === undefined) throw new RangeError('LLVM match cleanup lost its local type') + return Object.freeze([ + Object.freeze({ + _tag: 'BindMatch' as const, + scrutinee: match.scrutinee, + shape: match.scrutineeShape, + member: bindingMember, + destination: entry.destination, + path: entry.path, + type, + provenance: arm.provenance, + }), + Object.freeze({ + _tag: 'Drop' as const, + local: entry.destination, + cleanup: entry.cleanup, + provenance: arm.provenance, + }), + ]) + }) lowerSequence( selected, origin, 'Normal', [ ...arm.selected.operations, + ...cleanup, Object.freeze({ _tag: 'Move' as const, destination: match.destination, diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 23f60ebf6..047a64436 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -862,6 +862,7 @@ export const operationLocals = (operation: Operation): ReadonlyArray => ...arm.bindings.map((binding) => binding.destination), ...(arm.guard === undefined ? [] : [arm.guard.result]), arm.selected.result, + ...arm.selected.cleanup.map((entry) => entry.destination), ]), ] case 'Conditional': @@ -4559,8 +4560,11 @@ export const verify = (self: Module): ReadonlyArray => { entry.path, ) : coverageFieldPathType(self.layout, arm.member, entry.path) + const destinationType = fn.localTypes.at(entry.destination.ordinal) return ( selected !== undefined && + destinationType !== undefined && + SilkType.equals(semanticType(destinationType), entry.cleanup.type) && (arm.member?._tag === 'NominalUnionVariant' ? cleanupMatchesSemanticType(self.layout, entry.cleanup, selected) : SilkType.equals(selected, entry.cleanup.type)) diff --git a/packages/compiler/src/NativeValueOperation.ts b/packages/compiler/src/NativeValueOperation.ts index 0e08de255..ca5edb276 100644 --- a/packages/compiler/src/NativeValueOperation.ts +++ b/packages/compiler/src/NativeValueOperation.ts @@ -48,17 +48,13 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op const checkOrdinal = context.state.checkOrdinal switch (operation._tag) { case 'BindMatch': { - const physical = Layout.coverageFieldSlots( - operation.shape, - operation.member, - operation.binding.path, - ) + const physical = Layout.coverageFieldSlots(operation.shape, operation.member, operation.path) if (physical === undefined) { throw new RangeError('LLVM match lost a pattern payload path') } const source = NativeStorage.readLocal(nativeStorage, operation.scrutinee) const sourceLanes = operation.shape.lanes - const targetLanes = NativeType.lanesFor(types, operation.binding.type) + const targetLanes = NativeType.lanesFor(types, operation.type) const selected: Array = [] for (const [targetOrdinal, ordinal] of physical.entries()) { const value = source.at(ordinal) @@ -73,14 +69,14 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op value, sourceLane, targetLane, - `match${operation.binding.destination.ordinal}_${targetOrdinal}_lane`, + `match${operation.destination.ordinal}_${targetOrdinal}_lane`, ), ) } if (selected.length !== targetLanes.length) { throw new RangeError('LLVM match binding disagrees with its payload lanes') } - nativeStorage.locals.set(operation.binding.destination.ordinal, Object.freeze(selected)) + nativeStorage.locals.set(operation.destination.ordinal, Object.freeze(selected)) break } case 'EnumConstant': { diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 91129cdbe..02be3aa7e 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '6e2aa6ae1e7a0d1801e574baec23d1290fc05395755aeff48fdc064964d05081' +export const compilerDigest = 'e6acbb6d0ca94cc4fd525b782dff91ce6d786deb900e87e32aa2d662ecc6acb2' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 74433e0ce..4c860096d 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -4752,7 +4752,7 @@ const emitMatchOperation = ( operation: Extract, state: WasmOperationContext, ): ReadonlyArray => { - const { emitter, layout, suspension, slots, scalar, copy } = state + const { emitter, layout, suspension, slots, scalar, copy, releaseInstructions } = state const emitMany = (operations: ReadonlyArray): ReadonlyArray => operations.flatMap((nested) => emitOperation(nested, emitter, suspension)) @@ -4786,8 +4786,29 @@ const emitMatchOperation = ( slots(binding.destination), ) }) + const cleanup = arm.selected.cleanup.flatMap((entry) => { + const physical = LayoutPlan.coverageFieldSlots( + operation.scrutineeShape, + bindingMember, + entry.path, + ) + if (physical === undefined) { + throw new RangeError('Wasm match lost an omitted payload cleanup path') + } + return [ + ...copy( + physical.flatMap((lane) => { + const source = slots(operation.scrutinee).at(lane) + return source === undefined ? [] : [source] + }), + slots(entry.destination), + ), + ...releaseInstructions(entry.cleanup, entry.destination), + ] + }) const selected = [ ...emitMany(arm.selected.operations), + ...cleanup, ...(layout.types.at(arm.selected.result.ordinal)?._tag === 'Bottom' ? [] : copy(slots(arm.selected.result), slots(operation.destination))), diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 677b0799b..16465bb66 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -104,6 +104,16 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan : []), ] : []), + ...(operation._tag === 'Match' + ? operation.arms.flatMap((arm) => + arm.selected.cleanup.flatMap((entry) => + CleanupPlan.hasHook(entry.cleanup) && + fn.localTypes.at(entry.destination.ordinal)?._tag !== 'EffectBorrow' + ? [entry.destination.ordinal] + : [], + ), + ) + : []), ] }), ]) diff --git a/packages/compiler/test/Analysis.test.ts b/packages/compiler/test/Analysis.test.ts index 98a68a2de..d20b109ce 100644 --- a/packages/compiler/test/Analysis.test.ts +++ b/packages/compiler/test/Analysis.test.ts @@ -758,6 +758,7 @@ it.effect('publishes sealed local-shared identity, affinity, and structural obli const source = `struct Token { value: i32 } struct Generic { value: T } struct Pair { first: Intrinsic.SharedCore second: Intrinsic.SharedCore } +union LocalHolder { Empty, Full { core: Intrinsic.SharedCore } } struct LocalWrap { core: Intrinsic.SharedCore } struct Damaged { first: Missing second: AlsoMissing } struct Shared { value: i32 } @@ -835,6 +836,17 @@ pub fn main() -> i32 { return 0 }` ) assert.strictEqual(ExecutionAffinity.ofType(self.index, pair)._tag, 'LocalExecution') assert.strictEqual(LocalSharedOwnership.count(LocalSharedOwnership.ofType(self.index, pair)), 2) + const localHolder = Type.nominal('main', 'LocalHolder', ['i32']) + assert.strictEqual(ExecutionAffinity.ofType(self.index, localHolder)._tag, 'LocalExecution') + const localHolderObligations = LocalSharedOwnership.ofType(self.index, localHolder) + assert.strictEqual(localHolderObligations._tag, 'ActiveNominalUnion') + if (localHolderObligations._tag !== 'ActiveNominalUnion') return + assert.deepEqual( + localHolderObligations.cases.map((_, ordinal) => + LocalSharedOwnership.count(localHolderObligations, ordinal), + ), + [0, 1], + ) assert.strictEqual( LocalSharedOwnership.count( LocalSharedOwnership.ofType(self.index, Type.fixedArray(Type.sharedCore('i32'), 2)), @@ -1022,6 +1034,7 @@ it.effect('publishes sealed affine Execution identity, affinity, and logical lif ascii( `struct Execution { value: T } struct NestedLoan { value: &i32 } +union NestedUnionLoan { Empty, Ready { value: &i32 } } fn retain(value: Intrinsic.Execution) -> () { drop value } fn ordinary(value: Execution) -> () { drop value } pub fn main() -> i32 { return 42 }`, @@ -1130,6 +1143,22 @@ pub fn main() -> i32 { return 42 }`, nested._tag === 'Unsatisfied' ? nested.causes.at(0)?.path.join(' -> ') : '', 'NestedLoan.value', ) + const nestedUnion = ExecutableProperty.detachedOfEnvironment(self.index, [ + { + ordinal: 0, + access: 'Take', + type: Type.nominal('execution-semantics', 'NestedUnionLoan'), + }, + ]) + assert.strictEqual(nestedUnion._tag, 'Unsatisfied') + assert.strictEqual( + nestedUnion._tag === 'Unsatisfied' ? nestedUnion.causes.at(0)?.reason : undefined, + 'NestedLoan', + ) + assert.include( + nestedUnion._tag === 'Unsatisfied' ? nestedUnion.causes.at(0)?.path.join(' -> ') : '', + 'NestedUnionLoan.Ready.value', + ) const duplicate = yield* Analysis.ofSource( 'execution-move', diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 1824931dd..20df88139 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -6,6 +6,7 @@ import * as Layout from '../src/Layout.js' import * as LayoutEncode from '../src/LayoutEncode.js' import * as Match from '../src/Match.js' import * as MirEncoding from '../src/MirEncoding.js' +import * as MirLinearization from '../src/MirLinearization.js' import * as MirVerification from '../src/MirVerification.js' import * as SourceFile from '../src/SourceFile.js' import * as SourceResolver from '../src/SourceResolver.js' @@ -359,6 +360,44 @@ pub fn main() -> i32 { return unwrap(Option.Some { value: 42 }) }`), }), ) +it.effect('cleans omitted fields of the selected nominal union variant', () => + Effect.gen(function* () { + const self = yield* Analysis.ofSourceRealized( + 'union-values/omitted-cleanup', + ascii(`struct Bomb {} +impl Drop for Bomb { + fn drop(self: &mut Bomb) -> () { let boom = 1 / 0 return () } +} +union State { Empty, Ready { value: i32, bomb: Bomb } } +fn consume(state: State) -> i32 { + return match move state { + State.Empty => 0 + State.Ready { value, .. } => value + } +} +pub fn main() -> i32 { + return consume(State.Ready { value: 42, bomb: Bomb {} }) +}`), + 'wasm32-unknown-unknown', + ) + + assert.deepEqual(Analysis.diagnostics(self), []) + const consume = Analysis.loweredMir(self).functions.find((fn) => fn.id.name === 'consume') + assert.isTrue( + consume !== undefined && + MirLinearization.linearize(consume).some((block) => + block.operations.some( + (operation) => operation._tag === 'Drop' && operation.cleanup._tag === 'HookCleanup', + ), + ), + ) + assert.strictEqual(Analysis.evaluate(self)._tag, 'Trap') + const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.throws(() => (instance.exports.silk_main as () => number)(), WebAssembly.RuntimeError) + }), +) + it.effect('keeps nominal variants nested beneath structural union roots', () => Effect.gen(function* () { const self = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/Suspendability.test.ts b/packages/compiler/test/Suspendability.test.ts index 2e42e3f36..aa3542417 100644 --- a/packages/compiler/test/Suspendability.test.ts +++ b/packages/compiler/test/Suspendability.test.ts @@ -384,6 +384,42 @@ pub fn main() -> i32 { }), ) +it.effect('follows represented executables nested inside nominal union variants', () => + Effect.gen(function* () { + const source = `struct Box { value: i32 } +union Deferred> { Empty, Ready { operation: F } } +fn requireDetached + Intrinsic.Detached>(body: F) -> i32 { + drop body + return 1 +} +pub fn main() -> i32 { + let box = Box { value: 42 } + let view = &box + let deferred = Deferred.Ready { operation: effect { return view.value } } + return requireDetached(effect { drop move deferred return 1 }) +}` + const self = yield* snapshot(source) + + const diagnostics = Analysis.diagnostics(self) + assert.deepEqual( + diagnostics.map((diagnostic) => diagnostic.code), + ['SEM0139'], + ) + const diagnostic = diagnostics.at(0) + assert.strictEqual(diagnostic?.reason._tag, 'UnsatisfiedExecutableProperty') + assert.strictEqual( + diagnostic?.reason._tag === 'UnsatisfiedExecutableProperty' + ? diagnostic.reason.property + : undefined, + 'Intrinsic.Detached', + ) + assert.isTrue( + diagnostic?.reason._tag === 'UnsatisfiedExecutableProperty' && + diagnostic.reason.causes.some((cause) => cause.startsWith('LexicalLoan:')), + ) + }), +) + it.effect('closes direct self and mutual cycles over exact execution nodes', () => Effect.gen(function* () { const direct = yield* snapshot(`import silk.effect as Effect From 4df75c64a87a056fdc66c17e3b7f00c1777ff334 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 21:38:33 -0300 Subject: [PATCH 31/42] fix(compiler): clean whole nominal match values --- packages/compiler/src/LowerExpression.ts | 17 ++++++++++++++++- packages/compiler/src/LowerStatements.ts | 14 +++++++++++++- packages/compiler/src/NativeValueOperation.ts | 4 +++- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/test/StructValues.test.ts | 14 ++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/LowerExpression.ts b/packages/compiler/src/LowerExpression.ts index dfe5fe3e1..78f70b783 100644 --- a/packages/compiler/src/LowerExpression.ts +++ b/packages/compiler/src/LowerExpression.ts @@ -1525,10 +1525,25 @@ export function lowerExpressionInner( const ownedArm = ownership?.arms.find( (candidate) => candidate.id.ordinal === arm.id.ordinal, ) + const finalizedSelectedOperations = [...selectedOperations] const cleanup: Array = [] for (const release of ownedArm?.cleanup ?? []) { const plan = specializedCleanup(fn, release.cleanup) if (plan._tag === 'NoCleanup') continue + if ( + release.path.length === 0 && + Type.equals(plan.type, fn.semantic(expression.scrutinee.type)) + ) { + finalizedSelectedOperations.push( + Object.freeze({ + _tag: 'Drop', + local: scrutinee.result, + cleanup: plan, + provenance: authored(arm.span), + }), + ) + continue + } const type = fn.type(plan.type) if (type === undefined) return undefined cleanup.push( @@ -1550,7 +1565,7 @@ export function lowerExpressionInner( ...(guard === undefined ? {} : { guard }), selected: Object.freeze({ access: expression.access, - operations: selectedOperations, + operations: Object.freeze(finalizedSelectedOperations), result: selectedResult.result, cleanup: Object.freeze(cleanup), endBorrow: expression.access === 'Shared' || expression.access === 'Exclusive', diff --git a/packages/compiler/src/LowerStatements.ts b/packages/compiler/src/LowerStatements.ts index cce74ee03..222c53572 100644 --- a/packages/compiler/src/LowerStatements.ts +++ b/packages/compiler/src/LowerStatements.ts @@ -131,10 +131,22 @@ export const lowerPatternSelection = ( const ownedArm = ownership?.arms.find( (candidate) => candidate.id.ordinal === selection.arm.ordinal, ) + const finalizedSelectedOperations = [...selectedOperations] const cleanup: Array = [] for (const release of ownedArm?.cleanup ?? []) { const plan = specializedCleanup(fn, release.cleanup) if (plan._tag === 'NoCleanup') continue + if (release.path.length === 0 && Type.equals(plan.type, semanticSubject)) { + finalizedSelectedOperations.push( + Object.freeze({ + _tag: 'Drop', + local: subject.result, + cleanup: plan, + provenance: authored(selection.span), + }), + ) + continue + } const type = fn.type(plan.type) if (type === undefined) return undefined cleanup.push( @@ -154,7 +166,7 @@ export const lowerPatternSelection = ( bindings: Object.freeze(selectedBindings), selected: Object.freeze({ access: selection.access, - operations: selectedOperations, + operations: Object.freeze(finalizedSelectedOperations), result: selectedResult.result, cleanup: Object.freeze(cleanup), endBorrow: false, diff --git a/packages/compiler/src/NativeValueOperation.ts b/packages/compiler/src/NativeValueOperation.ts index ca5edb276..66651b80c 100644 --- a/packages/compiler/src/NativeValueOperation.ts +++ b/packages/compiler/src/NativeValueOperation.ts @@ -74,7 +74,9 @@ export const emit = Effect.fnUntraced(function* (context: Context, operation: Op ) } if (selected.length !== targetLanes.length) { - throw new RangeError('LLVM match binding disagrees with its payload lanes') + throw new RangeError( + `LLVM match binding %${operation.destination.ordinal} disagrees with its payload lanes (${physical.length} selected, ${targetLanes.length} required)`, + ) } nativeStorage.locals.set(operation.destination.ordinal, Object.freeze(selected)) break diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 02be3aa7e..ba24ee594 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'e6acbb6d0ca94cc4fd525b782dff91ce6d786deb900e87e32aa2d662ecc6acb2' +export const compilerDigest = 'f6bd9d74fe5a2e725b2c319593975f50459943fd2de4dcb3b3149832d9b2c8c0' diff --git a/packages/compiler/test/StructValues.test.ts b/packages/compiler/test/StructValues.test.ts index 20df88139..401b4440a 100644 --- a/packages/compiler/test/StructValues.test.ts +++ b/packages/compiler/test/StructValues.test.ts @@ -375,7 +375,11 @@ fn consume(state: State) -> i32 { State.Ready { value, .. } => value } } +fn ignore(state: State) -> i32 { + return match move state { _ => 42 } +} pub fn main() -> i32 { + if ignore(State.Empty) != 42 { return 0 } return consume(State.Ready { value: 42, bomb: Bomb {} }) }`), 'wasm32-unknown-unknown', @@ -383,6 +387,7 @@ pub fn main() -> i32 { assert.deepEqual(Analysis.diagnostics(self), []) const consume = Analysis.loweredMir(self).functions.find((fn) => fn.id.name === 'consume') + const ignore = Analysis.loweredMir(self).functions.find((fn) => fn.id.name === 'ignore') assert.isTrue( consume !== undefined && MirLinearization.linearize(consume).some((block) => @@ -391,6 +396,15 @@ pub fn main() -> i32 { ), ), ) + assert.isTrue( + ignore !== undefined && + MirLinearization.linearize(ignore).some((block) => + block.operations.some( + (operation) => + operation._tag === 'Drop' && operation.cleanup._tag === 'NominalUnionCleanup', + ), + ), + ) assert.strictEqual(Analysis.evaluate(self)._tag, 'Trap') const wasm = yield* Analysis.codegenWasm(self, { mode: 'release' }) const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) From 27a18326dc6d14e3b9ac78bed70c4acd05d29377 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 21:44:55 -0300 Subject: [PATCH 32/42] test(compiler): refresh logging MIR spans --- packages/compiler/test/goldens/logging.mir.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/goldens/logging.mir.txt b/packages/compiler/test/goldens/logging.mir.txt index 46e239c4c..c3a495242 100644 --- a/packages/compiler/test/goldens/logging.mir.txt +++ b/packages/compiler/test/goldens/logging.mir.txt @@ -480,8 +480,8 @@ fn silk/logger.record params=3 locals=4 -> once Effect<() ! silk/logger.LogError return %3 [7718, 8870) fn silk/string.utf8Bytes params=1 locals=2 -> &[u8] entry=r0 r0 operation: - %1 = string-utf8-bytes %0 loans=none : &[u8] [10437, 10470) - return %1 [10437, 10470) + %1 = string-utf8-bytes %0 loans=none : &[u8] [10465, 10498) + return %1 [10465, 10498) fn silk/u8.toI32 params=1 locals=2 -> i32 entry=r0 r0 operation: %1 = convert %0 u8 -> i32 [4964, 4989) From 731b1fd50ebd5a1dd161ed449f4ac2e849572c79 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 21:58:48 -0300 Subject: [PATCH 33/42] fix(vscode): highlight nominal union declarations --- apps/vscode/syntaxes/silk.tmLanguage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/syntaxes/silk.tmLanguage.json b/apps/vscode/syntaxes/silk.tmLanguage.json index 84296e276..a3b9f5328 100644 --- a/apps/vscode/syntaxes/silk.tmLanguage.json +++ b/apps/vscode/syntaxes/silk.tmLanguage.json @@ -329,7 +329,7 @@ }, { "name": "storage.type.silk", - "match": "\\b(?:pub|struct|enum|service|interface|role|effect|run|fail|drop|unsafe|impl|for|import|as|let|const|mut|once|move)\\b" + "match": "\\b(?:pub|struct|enum|union|service|interface|role|effect|run|fail|drop|unsafe|impl|for|import|as|let|const|mut|once|move)\\b" }, { "name": "constant.language.boolean.silk", From 962c290d1ac0abacbc9c834410adc9ac9c744b7f Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:03:56 -0300 Subject: [PATCH 34/42] test: validate nominal unions in release artifacts --- openspec/changes/add-nominal-unions/tasks.md | 8 ++++---- release-candidate/validate.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 1283f79bd..5c69e3a76 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -92,10 +92,10 @@ ## 12. Final Verification -- [ ] 12.1 Run the focused lexer, parser, formatter, declaration, semantic, matching, ownership, layout, HIR, MIR, evaluator, Wasm, native-corpus, intrinsic, stdlib, LSP, and doctest suites and verify every delta-spec scenario has direct evidence. +- [x] 12.1 Run the focused lexer, parser, formatter, declaration, semantic, matching, ownership, layout, HIR, MIR, evaluator, Wasm, native-corpus, intrinsic, stdlib, LSP, and doctest suites and verify every delta-spec scenario has direct evidence. - [x] 12.2 Run `pnpm typecheck` and fix every introduced type error, recording any unrelated pre-existing failure exactly. - [x] 12.3 Run `pnpm exec biome check .` and fix every introduced formatting or lint failure, recording any unrelated pre-existing failure exactly. -- [ ] 12.4 Run `pnpm test` and fix every introduced test failure, recording any unrelated pre-existing failure exactly. -- [ ] 12.5 Run `pnpm check` and verify the repository-wide required gate completes, or report the exact pre-existing blocker without describing the change as complete. -- [ ] 12.6 Run `pnpm release:candidate` because compiler package contents change, and verify package contents, exports, stdlib embeddings, and release artifacts are internally consistent. +- [x] 12.4 Run `pnpm test` and fix every introduced test failure, recording any unrelated pre-existing failure exactly. +- [x] 12.5 Run `pnpm check` and verify the repository-wide required gate completes, or report the exact pre-existing blocker without describing the change as complete. +- [x] 12.6 Run `pnpm release:candidate` because compiler package contents change, and verify package contents, exports, stdlib embeddings, and release artifacts are internally consistent. - [x] 12.7 Run `openspec validate add-nominal-unions --strict` and verify proposal, all delta specs, design, and tasks remain coherent after implementation discoveries. diff --git a/release-candidate/validate.test.ts b/release-candidate/validate.test.ts index c0cad4608..c94493829 100644 --- a/release-candidate/validate.test.ts +++ b/release-candidate/validate.test.ts @@ -432,13 +432,13 @@ test('the compiler release candidate exposes only its bootstrap ESM actors', () 'pub service Logger', ) expect(readFileSync(resolve(packedRoot, 'stdlib/silk/option.silk'), 'utf8')).toContain( - 'pub struct Option', + 'pub union Option', ) expect(readFileSync(resolve(packedRoot, 'stdlib/silk/os_filesystem.silk'), 'utf8')).toContain( 'pub struct OsFileSystem', ) expect(readFileSync(resolve(packedRoot, 'stdlib/silk/result.silk'), 'utf8')).toContain( - 'pub struct Result', + 'pub union Result', ) expect( readFileSync(resolve(packedRoot, 'stdlib/silk/standard_streams.silk'), 'utf8'), From 5bb8613115a0d2396dc296320b856d37adc0e199 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:09:20 -0300 Subject: [PATCH 35/42] test(stdlib): execute alternate result success --- packages/compiler/test/ResultStdlib.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/compiler/test/ResultStdlib.test.ts b/packages/compiler/test/ResultStdlib.test.ts index 90a2ae55f..f5844e8e6 100644 --- a/packages/compiler/test/ResultStdlib.test.ts +++ b/packages/compiler/test/ResultStdlib.test.ts @@ -150,13 +150,14 @@ effect fn outcome( ) } -effect fn choose(first: bool) -> i32 ! First | Second { - if first { fail First { code: 20 } } +effect fn choose(kind: i32) -> i32 ! First | Second { + if kind == 0 { return 5 } + if kind == 1 { fail First { code: 20 } } fail Second { code: 22 } } -effect fn inspect(first: bool) -> i32 { - let completed = run outcome(choose(first)) +effect fn inspect(kind: i32) -> i32 { + let completed = run outcome(choose(kind)) return match move completed { Outcome.Good { value } => value Outcome.Bad { error } => match move error { @@ -167,9 +168,10 @@ effect fn inspect(first: bool) -> i32 { } pub fn main() -> i32 { - let first = run inspect(true) - let second = run inspect(false) - return first + second + let success = run inspect(0) + let first = run inspect(1) + let second = run inspect(2) + return success + first + second - 5 }` const reifiedTrap = `import silk.effect as Effect From 8464d109cba7c3da68640dcdf327237cbfd13212 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:20:51 -0300 Subject: [PATCH 36/42] fix(compiler): close nominal union review gaps --- packages/compiler/src/Completion.ts | 2 + packages/compiler/src/DeclarationFacts.ts | 8 +- packages/compiler/src/Elaboration.ts | 2 +- packages/compiler/src/Layout.ts | 68 ++++++++++++++--- packages/compiler/src/LayoutEncode.ts | 9 ++- packages/compiler/src/LayoutVerify.ts | 75 +++++++++++++------ packages/compiler/src/MirVerification.ts | 21 +++++- packages/compiler/src/ModuleSummary.ts | 4 +- packages/compiler/src/StatementAnalysis.ts | 1 + .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/test/AutoImport.test.ts | 2 +- .../compiler/test/EditorIntelligence.test.ts | 23 ++++++ packages/compiler/test/Elaboration.test.ts | 26 +++++++ packages/compiler/test/Logging.test.ts | 9 +++ packages/compiler/test/ModuleSummary.test.ts | 8 +- packages/compiler/test/SlotLaneWidth.test.ts | 56 ++++++++++++++ .../StoredEffectCleanupVerification.test.ts | 42 +++++++++++ 17 files changed, 312 insertions(+), 46 deletions(-) diff --git a/packages/compiler/src/Completion.ts b/packages/compiler/src/Completion.ts index 4ccc00fa9..cee27bf66 100644 --- a/packages/compiler/src/Completion.ts +++ b/packages/compiler/src/Completion.ts @@ -464,6 +464,7 @@ const typeCandidates = ( ) for (const declaration of [ ...(index.modules.find((headers) => headers.module === module)?.structs ?? []), + ...(index.modules.find((headers) => headers.module === module)?.unions ?? []), ...(index.modules.find((headers) => headers.module === module)?.enums ?? []), ...(index.modules.find((headers) => headers.module === module)?.services ?? []), ...(index.modules.find((headers) => headers.module === module)?.interfaces ?? []), @@ -494,6 +495,7 @@ const typeCandidates = ( const declaration = DeclarationFacts.byCanonical(index, binding.declaration) if ( declaration?._tag !== 'StructDeclaration' && + declaration?._tag !== 'UnionDeclaration' && declaration?._tag !== 'EnumDeclaration' && declaration?._tag !== 'ServiceDeclaration' && declaration?._tag !== 'InterfaceDeclaration' diff --git a/packages/compiler/src/DeclarationFacts.ts b/packages/compiler/src/DeclarationFacts.ts index 0286f5737..1c3984f63 100644 --- a/packages/compiler/src/DeclarationFacts.ts +++ b/packages/compiler/src/DeclarationFacts.ts @@ -1793,7 +1793,7 @@ export const containsLexicalBorrow = ( module: type.module, name: type.name, }) - if (declaration?._tag !== 'StructDeclaration') + if (declaration?._tag !== 'StructDeclaration' && declaration?._tag !== 'UnionDeclaration') return type.arguments .filter(Type.isTypeArgument) .some((argument) => containsLexicalBorrow(self, argument, seen)) @@ -1803,7 +1803,11 @@ export const containsLexicalBorrow = ( type.arguments, ) ?? new Map() const next = new Set(seen).add(key) - return declaration.fields.some( + const fields = + declaration._tag === 'StructDeclaration' + ? declaration.fields + : declaration.variants.flatMap((variant) => variant.fields) + return fields.some( (field) => field.declaredType._tag === 'Resolved' && containsLexicalBorrow(self, Type.substitute(field.declaredType.type, substitution), next), diff --git a/packages/compiler/src/Elaboration.ts b/packages/compiler/src/Elaboration.ts index 928337dd5..5b6690393 100644 --- a/packages/compiler/src/Elaboration.ts +++ b/packages/compiler/src/Elaboration.ts @@ -1727,7 +1727,7 @@ const constrainedCallableEscapeDiagnostics = ( reject(statement._tag === 'ReturnStatement' ? statement.expression : statement.value) }, expression: (expression) => { - if (expression._tag === 'StructLiteral') { + if (expression._tag === 'StructLiteral' || expression._tag === 'UnionVariant') { for (const initializer of expression.initializers) if (constrainedCallableSchema(initializer.expression) !== undefined) reject(initializer.expression) diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 3224c3988..3200981a8 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -44,6 +44,12 @@ export interface Field extends PlacedField { readonly type: DeclarationFacts.SemanticType } +/** Static cleanup hook required before structural cleanup; contributes no ABI bytes. */ +export interface CleanupHook { + readonly hook: DeclarationFacts.CanonicalId + readonly typeArguments: ReadonlyArray +} + /** The initial closed representation vocabulary for concrete runtime types. */ export type Representation = | { readonly _tag: 'SignedInteger'; readonly bits: Scalar.FixedBits } @@ -65,11 +71,7 @@ export type Representation = readonly _tag: 'Aggregate' readonly fields: ReadonlyArray readonly tailPadding: number - /** Static cleanup hook required before structural field cleanup; contributes no ABI bytes. */ - readonly cleanupHook?: { - readonly hook: DeclarationFacts.CanonicalId - readonly typeArguments: ReadonlyArray - } + readonly cleanupHook?: CleanupHook } | { readonly _tag: 'CallableEnvironment' @@ -166,6 +168,7 @@ export type Representation = readonly payloadAlignment: number readonly tagPadding: number readonly tailPadding: number + readonly cleanupHook?: CleanupHook } /** One compiler-owned concrete layout entry. */ @@ -1303,18 +1306,55 @@ export const catalog = ( completed.set(key, failure) return failure } - const payloadAlignment = variants.reduce( - (maximum, variant) => Math.max(maximum, variant.alignment), - 1, + const callingEntries = new Map( + [...completed].flatMap(([entryKey, candidate]) => + candidate._tag === 'LayoutEntry' ? [[entryKey, candidate] as const] : [], + ), ) - const payloadSize = variants.reduce((maximum, variant) => Math.max(maximum, variant.size), 0) + const callingContext = Object.freeze({ + target, + entries: callingEntries, + effectEnvironments: Object.freeze([]), + callableEnvironments: Object.freeze([]), + active: new Set(), + }) + const variantShapes = variants.map((variant): CallingShapeNode => { + const fields = Object.freeze( + variant.fields.map((field) => + Object.freeze({ field: field.id, shape: shapeNode(field.type, callingContext) }), + ), + ) + return Object.freeze({ + _tag: 'ProductShape', + type, + fields, + laneCount: fields.reduce((total, field) => total + field.shape.laneCount, 0), + }) + }) + const payloadTypes = unifyPayloadTypes(variantShapes, target) + const payload = Packing.pack( + payloadTypes.map((payloadType) => { + const scalar = scalarEntry(target, payloadType) + return Object.freeze({ + value: payloadType, + size: scalar.size, + alignment: scalar.alignment, + }) + }), + ) + const payloadAlignment = payload.alignment + const payloadSize = payload.size const payloadOffset = alignUp(4, payloadAlignment) const alignment = Math.max(4, payloadAlignment) const size = alignUp(payloadOffset + payloadSize, alignment) + const cleanup = CleanupPlan.cleanupPlan(index, type) const entry: Entry = Object.freeze({ _tag: 'LayoutEntry', type, - copy: ConformanceProof.hasCopyDeclaration(index, type) && fieldsCopy, + copy: + ConformanceProof.hasCopyDeclaration(index, type) && + fieldsCopy && + cleanup._tag !== 'HookCleanup', size, alignment, representation: Object.freeze({ @@ -1327,6 +1367,14 @@ export const catalog = ( payloadAlignment, tagPadding: payloadOffset - 4, tailPadding: size - (payloadOffset + payloadSize), + ...(cleanup._tag === 'HookCleanup' + ? { + cleanupHook: Object.freeze({ + hook: cleanup.hook, + typeArguments: cleanup.typeArguments, + }), + } + : {}), }), }) completed.set(key, entry) diff --git a/packages/compiler/src/LayoutEncode.ts b/packages/compiler/src/LayoutEncode.ts index 1844ad711..d1cf65d17 100644 --- a/packages/compiler/src/LayoutEncode.ts +++ b/packages/compiler/src/LayoutEncode.ts @@ -47,8 +47,13 @@ const representationText = (representation: Representation): string => { return `reference target=${Type.encode(representation.target)} address=i${representation.address.bits}@${representation.address.offset}/${representation.address.size}/${representation.address.alignment}` case 'Union': return `union tag=i${representation.tag.bits} payload-offset=${representation.payloadOffset} payload-size=${representation.payloadSize} payload-align=${representation.payloadAlignment} tag-padding=${representation.tagPadding} tail-padding=${representation.tailPadding}` - case 'NominalUnion': - return `nominal-union ${representation.union.module}.${representation.union.name} tag=i${representation.tag.bits} payload-offset=${representation.payloadOffset} payload-size=${representation.payloadSize} payload-align=${representation.payloadAlignment} tag-padding=${representation.tagPadding} tail-padding=${representation.tailPadding}` + case 'NominalUnion': { + const cleanupHook = + representation.cleanupHook === undefined + ? 'none' + : `${representation.cleanupHook.hook.module}.${representation.cleanupHook.hook.name}<${representation.cleanupHook.typeArguments.map(Type.encodeGenericArgument).join(',')}>` + return `nominal-union ${representation.union.module}.${representation.union.name} tag=i${representation.tag.bits} payload-offset=${representation.payloadOffset} payload-size=${representation.payloadSize} payload-align=${representation.payloadAlignment} tag-padding=${representation.tagPadding} tail-padding=${representation.tailPadding} cleanup-hook=${cleanupHook}` + } case 'Aggregate': { const cleanupHook = representation.cleanupHook === undefined diff --git a/packages/compiler/src/LayoutVerify.ts b/packages/compiler/src/LayoutVerify.ts index 390095b04..904b26c1c 100644 --- a/packages/compiler/src/LayoutVerify.ts +++ b/packages/compiler/src/LayoutVerify.ts @@ -29,6 +29,21 @@ import * as Scalar from './Scalar.js' import * as Target from './Target.js' import * as Type from './Type.js' +const cleanupHooksEqual = ( + leftHook: Extract['cleanupHook'], + rightHook: Extract['cleanupHook'], +): boolean => + leftHook === undefined + ? rightHook === undefined + : rightHook !== undefined && + leftHook.hook.module === rightHook.hook.module && + leftHook.hook.name === rightHook.hook.name && + leftHook.typeArguments.length === rightHook.typeArguments.length && + leftHook.typeArguments.every((argument, ordinal) => { + const other = rightHook.typeArguments.at(ordinal) + return other !== undefined && Type.equalsGenericArgument(argument, other) + }) + const representationEquals = (left: Representation, right: Representation): boolean => { if (left._tag !== right._tag) return false if (left._tag === 'SignedInteger') @@ -195,6 +210,7 @@ const representationEquals = (left: Representation, right: Representation): bool left.payloadAlignment === right.payloadAlignment && left.tagPadding === right.tagPadding && left.tailPadding === right.tailPadding && + cleanupHooksEqual(left.cleanupHook, right.cleanupHook) && left.variants.length === right.variants.length && left.variants.every((variant, ordinal) => { const other = right.variants.at(ordinal) @@ -225,20 +241,6 @@ const representationEquals = (left: Representation, right: Representation): bool }) ) } - const cleanupHooksEqual = ( - leftHook: Extract['cleanupHook'], - rightHook: Extract['cleanupHook'], - ): boolean => - leftHook === undefined - ? rightHook === undefined - : rightHook !== undefined && - leftHook.hook.module === rightHook.hook.module && - leftHook.hook.name === rightHook.hook.name && - leftHook.typeArguments.length === rightHook.typeArguments.length && - leftHook.typeArguments.every((argument, ordinal) => { - const other = rightHook.typeArguments.at(ordinal) - return other !== undefined && Type.equalsGenericArgument(argument, other) - }) return ( right._tag === 'Aggregate' && cleanupHooksEqual(left.cleanupHook, right.cleanupHook) && @@ -759,6 +761,7 @@ const verifyEntry = ( ), ) } + let variantFieldsValid = true for (const [ordinal, variant] of representation.variants.entries()) { const expected = variant.fields.map((field) => { const fieldLayout = Type.isBuiltin(field.type) @@ -793,6 +796,7 @@ const verifyEntry = ( variant.alignment !== packed.alignment || variant.tailPadding !== packed.tailPadding ) { + variantFieldsValid = false unionViolations.push( invalid( 'InvalidAggregate', @@ -802,14 +806,24 @@ const verifyEntry = ( ) } } - const payloadAlignment = representation.variants.reduce( - (maximum, variant) => Math.max(maximum, variant.alignment), - 1, - ) - const payloadSize = representation.variants.reduce( - (maximum, variant) => Math.max(maximum, variant.size), - 0, - ) + const unionShape = variantFieldsValid + ? callingShapes(target, [...available.values()], [candidate.type]).at(0)?.tree + : undefined + const payload = + unionShape?._tag === 'NominalUnionShape' + ? Packing.pack( + unionShape.payloadTypes.map((payloadType) => { + const scalar = scalarEntry(target, payloadType) + return Object.freeze({ + value: payloadType, + size: scalar.size, + alignment: scalar.alignment, + }) + }), + ) + : undefined + const payloadAlignment = payload?.alignment ?? 1 + const payloadSize = payload?.size ?? 0 const payloadOffset = alignUp(4, payloadAlignment) const alignment = Math.max(4, payloadAlignment) const size = alignUp(payloadOffset + payloadSize, alignment) @@ -830,6 +844,23 @@ const verifyEntry = ( ), ) } + const cleanupHook = representation.cleanupHook + if ( + cleanupHook !== undefined && + (cleanupHook.hook.module.length === 0 || + cleanupHook.hook.name.length === 0 || + cleanupHook.typeArguments.some( + (argument) => !Type.isRuntimeConcreteGenericArgument(argument), + )) + ) { + unionViolations.push( + invalid( + 'InvalidAggregate', + candidate.type, + `${Type.encode(candidate.type)} has a non-canonical cleanup hook`, + ), + ) + } return Object.freeze(unionViolations) } if (candidate.representation._tag !== 'Aggregate') { diff --git a/packages/compiler/src/MirVerification.ts b/packages/compiler/src/MirVerification.ts index 047a64436..778b0ee30 100644 --- a/packages/compiler/src/MirVerification.ts +++ b/packages/compiler/src/MirVerification.ts @@ -1413,13 +1413,28 @@ const cleanupMatchesSemanticType = ( if (seen.has(key)) return cleanup._tag === 'NoCleanup' const representation = Layout.entry(layout, type)?.representation if (representation?._tag === 'NominalUnion') { + const requiredHook = representation.cleanupHook + if (requiredHook !== undefined) { + if ( + cleanup._tag !== 'HookCleanup' || + cleanup.hook.module !== requiredHook.hook.module || + cleanup.hook.name !== requiredHook.hook.name || + cleanup.typeArguments.length !== requiredHook.typeArguments.length || + !cleanup.typeArguments.every((argument, ordinal) => { + const expected = requiredHook.typeArguments.at(ordinal) + return expected !== undefined && SilkType.equalsGenericArgument(argument, expected) + }) + ) + return false + } else if (cleanup._tag === 'HookCleanup') return false + const concrete = cleanup._tag === 'HookCleanup' ? cleanup.inner : cleanup if ( - cleanup._tag !== 'NominalUnionCleanup' || - cleanup.variants.length !== representation.variants.length + concrete._tag !== 'NominalUnionCleanup' || + concrete.variants.length !== representation.variants.length ) return false const next = new Set(seen).add(key) - return cleanup.variants.every((variant, ordinal) => { + return concrete.variants.every((variant, ordinal) => { const expected = representation.variants.at(ordinal) return ( expected !== undefined && diff --git a/packages/compiler/src/ModuleSummary.ts b/packages/compiler/src/ModuleSummary.ts index 3af41671f..eaf8c2045 100644 --- a/packages/compiler/src/ModuleSummary.ts +++ b/packages/compiler/src/ModuleSummary.ts @@ -9,7 +9,7 @@ import * as SyntaxTree from './SyntaxTree.js' export type Namespace = 'Value' | 'Type' | 'ValueAndType' /** A top-level declaration kind that can be named by a selected-member import. */ -export type DeclarationKind = 'Function' | 'Constant' | 'Struct' | 'Service' | 'Interface' +export type DeclarationKind = 'Function' | 'Constant' | 'Struct' | 'Union' | 'Service' | 'Interface' /** One compact public declaration header retained for exact-name candidate lookup. */ export interface Export { @@ -40,6 +40,8 @@ const declarationKind = ( return { declarationKind: 'Constant', namespace: 'Value' } case 'StructDeclaration': return { declarationKind: 'Struct', namespace: 'ValueAndType' } + case 'UnionDeclaration': + return { declarationKind: 'Union', namespace: 'ValueAndType' } case 'ServiceDeclaration': return { declarationKind: 'Service', namespace: 'Type' } case 'InterfaceDeclaration': diff --git a/packages/compiler/src/StatementAnalysis.ts b/packages/compiler/src/StatementAnalysis.ts index 809c80059..c9e06f050 100644 --- a/packages/compiler/src/StatementAnalysis.ts +++ b/packages/compiler/src/StatementAnalysis.ts @@ -77,6 +77,7 @@ export const isStaticallyDetachedFailure = ( case 'Move': return isStaticallyDetachedFailure(expression.subject, index) case 'StructLiteral': + case 'UnionVariant': return expression.fields.every((field) => isStaticallyDetachedFailure(field.initializer.expression, index), ) diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index ba24ee594..97421c73b 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'f6bd9d74fe5a2e725b2c319593975f50459943fd2de4dcb3b3149832d9b2c8c0' +export const compilerDigest = '3f1662afaebdb546b979c13804418b262f67a6bde186ebb484ec7d3a164f9313' diff --git a/packages/compiler/test/AutoImport.test.ts b/packages/compiler/test/AutoImport.test.ts index 89b7a3f4e..c4f9db12c 100644 --- a/packages/compiler/test/AutoImport.test.ts +++ b/packages/compiler/test/AutoImport.test.ts @@ -51,7 +51,7 @@ it.effect('filters candidates by semantic namespace and omits private declaratio project: [ ['main', summary('main', rootText)], ['wrong', summary('wrong', 'pub fn Wanted() -> i32 { return 1 }')], - ['right', summary('right', 'pub struct Wanted {}')], + ['right', summary('right', 'pub union Wanted { Value }')], ['private', summary('private', 'struct Wanted {}')], ], }) diff --git a/packages/compiler/test/EditorIntelligence.test.ts b/packages/compiler/test/EditorIntelligence.test.ts index 8ab75c14c..bffd89343 100644 --- a/packages/compiler/test/EditorIntelligence.test.ts +++ b/packages/compiler/test/EditorIntelligence.test.ts @@ -1677,6 +1677,7 @@ pub fn main() -> i32 { return ContractLogger. }` ) const typeSource = `struct Local {} +union LocalChoice { Empty } service Logger { fn enabled() -> bool } fn identity(value: ) -> i32 { return 0 }` const typeSnapshot = yield* Analysis.ofSourceRealized('main', encoder.encode(typeSource)) @@ -1687,11 +1688,33 @@ fn identity(value: ) -> i32 { return 0 }` ) assert.deepEqual(typeResult?.context, { _tag: 'DeclaredTypeContext' }) assert.include(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'Local') + assert.include(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'LocalChoice') assert.include(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'Logger') assert.include(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'f32') assert.include(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'f64') assert.include(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'string') assert.notInclude(typeResult?.candidates.map((candidate) => candidate.label) ?? [], 'true') + + const importedUnionSource = `import contracts { ContractChoice } +fn identity(value: ) -> i32 { return 0 }` + const importedUnion = yield* Analysis.makeRealized({ + root: SourceFile.make('main', encoder.encode(importedUnionSource)), + }).pipe( + Effect.provide( + SourceResolver.memory( + new Map([['contracts', encoder.encode('pub union ContractChoice { Empty }')]]), + ), + ), + ) + const importedUnionResult = Analysis.completionAt( + importedUnion, + 'main', + importedUnionSource.indexOf('value: ') + 'value: '.length, + ) + assert.include( + importedUnionResult?.candidates.map((candidate) => candidate.label) ?? [], + 'ContractChoice', + ) }), ) diff --git a/packages/compiler/test/Elaboration.test.ts b/packages/compiler/test/Elaboration.test.ts index 0238ba737..d69f14891 100644 --- a/packages/compiler/test/Elaboration.test.ts +++ b/packages/compiler/test/Elaboration.test.ts @@ -400,6 +400,32 @@ effect fn risky(error: OwnedError) -> i32 ! OwnedError { ) }) +it('checks lexical borrows through nominal union failure payloads', () => { + const dynamic = analyzeText( + 'effect://borrowed-union-failure', + `union BorrowedError { Value { message: string } } +effect fn risky(message: string) -> never ! BorrowedError { + fail BorrowedError.Value { message: message } +}`, + ) + assert.include( + dynamic.diagnostics.map((diagnostic) => diagnostic.code), + 'SEM0073', + ) + + const staticText = analyzeText( + 'effect://static-union-failure', + `union BorrowedError { Value { message: string } } +effect fn risky() -> never ! BorrowedError { + fail BorrowedError.Value { message: "program lifetime" } +}`, + ) + assert.notInclude( + staticText.diagnostics.map((diagnostic) => diagnostic.code), + 'SEM0073', + ) +}) + it('subtracts a provided capability role from an Effect contract', () => { const result = analyzeText( 'effect://provide-role', diff --git a/packages/compiler/test/Logging.test.ts b/packages/compiler/test/Logging.test.ts index 720b6666e..e7b920f7f 100644 --- a/packages/compiler/test/Logging.test.ts +++ b/packages/compiler/test/Logging.test.ts @@ -603,6 +603,15 @@ pub fn main() -> i32 { let mut logger = Logger.inMemoryProvider() let operation = invoke(Effect.provideMut(&mut logger), Effect.log("indirect")) return 42 +}`, + `import silk.effect as Effect +import silk.logger { InMemoryLogger } +import silk.logger { Logger } +union Store { Empty, Stored { value: F } } +pub fn main() -> i32 { + let mut logger = Logger.inMemoryProvider() + let escaped = Store.Stored { value: Effect.provideMut(&mut logger) } + return 42 }`, ] for (const [ordinal, body] of cases.entries()) { diff --git a/packages/compiler/test/ModuleSummary.test.ts b/packages/compiler/test/ModuleSummary.test.ts index 2eb752bd0..6823256a8 100644 --- a/packages/compiler/test/ModuleSummary.test.ts +++ b/packages/compiler/test/ModuleSummary.test.ts @@ -18,6 +18,7 @@ import app.log as Log pub fn execute() -> i32 { return 1 } fn helper() -> i32 { return 2 } pub struct User {} +pub union Choice { Empty } pub service Clock { fn now() -> i32 } pub interface Show { fn show() -> i32 } pub const answer: i32 = 42`, @@ -34,9 +35,10 @@ pub const answer: i32 = 42`, [ { spelling: 'execute', declarationKind: 'Function', namespace: 'Value', ordinal: 0 }, { spelling: 'User', declarationKind: 'Struct', namespace: 'ValueAndType', ordinal: 2 }, - { spelling: 'Clock', declarationKind: 'Service', namespace: 'Type', ordinal: 3 }, - { spelling: 'Show', declarationKind: 'Interface', namespace: 'Type', ordinal: 4 }, - { spelling: 'answer', declarationKind: 'Constant', namespace: 'Value', ordinal: 5 }, + { spelling: 'Choice', declarationKind: 'Union', namespace: 'ValueAndType', ordinal: 3 }, + { spelling: 'Clock', declarationKind: 'Service', namespace: 'Type', ordinal: 4 }, + { spelling: 'Show', declarationKind: 'Interface', namespace: 'Type', ordinal: 5 }, + { spelling: 'answer', declarationKind: 'Constant', namespace: 'Value', ordinal: 6 }, ], ) }) diff --git a/packages/compiler/test/SlotLaneWidth.test.ts b/packages/compiler/test/SlotLaneWidth.test.ts index f29ff063e..e9bff5246 100644 --- a/packages/compiler/test/SlotLaneWidth.test.ts +++ b/packages/compiler/test/SlotLaneWidth.test.ts @@ -151,6 +151,62 @@ it.effect('sums two u8 slots in one word to 14 on every engine', () => }), ) +it.effect('keeps widened nominal-union payload lanes inside each allocated element', () => + Effect.gen(function* () { + const source = `import silk.allocator { Allocator } +import silk.allocator { OutOfMemoryError } +import silk.allocator { SystemAllocator } +import silk.effect as Effect +import silk.i8 as i8 +import silk.layout { Layout } +import silk.raw_buffer as RawBuffer +import silk.slot as Slot + +union Choice { Small { first: i8, second: i8 }, Wide { value: i64 } } + +effect fn store() -> i32 ! OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + let layout = Layout.of<[Choice; 2]>() + let recipe = Allocator.allocate(move layout) |> Effect.provideMut(&mut allocator) + let allocation = run recipe + unsafe { + let mut buffer = RawBuffer.from(move allocation, 2) + let secondValue = Choice.Small { first: 17, second: 20 } + let secondWritten = Slot.write(RawBuffer.slot(&mut buffer, 1), move secondValue) + let firstValue = Choice.Small { first: 0, second: 0 } + let firstWritten = Slot.write(RawBuffer.slot(&mut buffer, 0), move firstValue) + let discarded = Slot.take(RawBuffer.slot(&mut buffer, 0)) + let selected = Slot.take(RawBuffer.slot(&mut buffer, 1)) + drop discarded + drop buffer + return match move selected { + Choice.Small { first, second } => i8.toI32(first) + i8.toI32(second) + Choice.Wide { value } => 0 + } + } + return 0 +} + +effect fn recover(error: OutOfMemoryError) -> i32 { return 0 } +pub fn main() -> i32 { return run Effect.catchAll(store(), recover) }` + const snapshot = yield* Analysis.ofSourceRealized( + 'slot-lane-width/nominal-union', + ascii(source), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual(evaluated._tag, 'Completed') + if (evaluated._tag !== 'Completed') return + assert.strictEqual(evaluated.result.value, 37n) + + const wasm = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 37) + }), +) + /** * Reading a field through a reference lands in the same lane-load helper the slot reads use, so * a struct packed with sub-word fields catches a fixed-width load there the same way: every diff --git a/packages/compiler/test/StoredEffectCleanupVerification.test.ts b/packages/compiler/test/StoredEffectCleanupVerification.test.ts index 3c880a241..f2c98902d 100644 --- a/packages/compiler/test/StoredEffectCleanupVerification.test.ts +++ b/packages/compiler/test/StoredEffectCleanupVerification.test.ts @@ -77,6 +77,48 @@ const replaceDrop = ( ), }) +it.effect('accepts Drop-wrapped nominal union cleanup with represented Effect fields', () => + Effect.gen(function* () { + const source = `union Deferred> { Empty, Ready { operation: F } } +impl> Drop for Deferred { + fn drop(self: &mut Deferred) -> () { return () } +} +fn consume>(value: Deferred) -> () { drop value } +pub fn main() -> i32 { + consume(Deferred.Ready { operation: effect { return 42 } }) + return 42 +}` + const snapshot = yield* Analysis.ofSourceRealized( + 'stored-effect-cleanup-verification/nominal-union-hook', + ascii(source), + Target.wasm32UnknownUnknown.id, + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + const module = Analysis.loweredMir(snapshot) + assert.deepEqual(MirVerification.verify(module), []) + const unionEntry = module.layout.entries.find( + (entry) => entry.representation._tag === 'NominalUnion', + ) + assert.isTrue( + unionEntry?.representation._tag === 'NominalUnion' && + unionEntry.representation.cleanupHook !== undefined, + ) + assert.isTrue( + module.functions + .flatMap(MirVerification.operations) + .some( + (operation) => + operation._tag === 'Drop' && + operation.cleanup._tag === 'HookCleanup' && + operation.cleanup.inner._tag === 'NominalUnionCleanup', + ), + ) + const wasm = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + it.effect('rejects incomplete cleanup inside nested Effect and callable captures', () => Effect.gen(function* () { const { catalog, module } = yield* lowerStored( From eb83ad43afc6f4e592fe4f30cc9f05fd27ad792b Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:22:29 -0300 Subject: [PATCH 37/42] fix(lsp): surface union auto imports --- packages/lsp/src/Document.ts | 4 +++- packages/lsp/test/Document.test.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/lsp/src/Document.ts b/packages/lsp/src/Document.ts index 7db826192..9c58e44a5 100644 --- a/packages/lsp/src/Document.ts +++ b/packages/lsp/src/Document.ts @@ -358,7 +358,7 @@ export const parseAutoImportData = (value: unknown): AutoImportData | undefined typeof candidate.module !== 'string' || typeof candidate.spelling !== 'string' || typeof candidate.ordinal !== 'number' || - !['Function', 'Constant', 'Struct', 'Service', 'Interface'].includes( + !['Function', 'Constant', 'Struct', 'Union', 'Service', 'Interface'].includes( typeof candidate.declarationKind === 'string' ? candidate.declarationKind : '', ) ) @@ -1482,6 +1482,8 @@ export const completion = ( return CompletionItemKind.Constant case 'Struct': return CompletionItemKind.Struct + case 'Union': + return CompletionItemKind.Enum case 'Service': case 'Interface': return CompletionItemKind.Interface diff --git a/packages/lsp/test/Document.test.ts b/packages/lsp/test/Document.test.ts index fe184e75d..70066cdcf 100644 --- a/packages/lsp/test/Document.test.ts +++ b/packages/lsp/test/Document.test.ts @@ -1600,7 +1600,7 @@ it.effect('completes catalog declarations with explicit collision-aware imports' const { document, snapshot } = yield* open(source) const inventory = inventoryOf([ { module: 'main', text: source }, - { module: 'silk/logger', text: 'pub service Logger {}' }, + { module: 'silk/logger', text: 'pub union Logger { Empty }' }, ]) const completion = Document.completion( document, @@ -1611,6 +1611,7 @@ it.effect('completes catalog declarations with explicit collision-aware imports' const imported = completion.items.find( (item) => item.label === 'Logger' && item.detail === 'Import from silk/logger', ) + assert.strictEqual(imported?.kind, CompletionItemKind.Enum) assert.deepEqual(imported?.textEdit, { range: { start: positionAt(source, source.indexOf('Logg')), From 8a4fd67574a07abed14eb2beb699802d553663d4 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:38:35 -0300 Subject: [PATCH 38/42] fix(wasm): materialize nominal union cleanup --- openspec/changes/add-nominal-unions/design.md | 22 +- .../specs/bootstrap-target-layout/spec.md | 29 +- openspec/changes/add-nominal-unions/tasks.md | 2 +- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 707 +++++++++++------- packages/compiler/src/WasmMemory.ts | 102 +++ packages/compiler/test/SlotLaneWidth.test.ts | 67 ++ 7 files changed, 627 insertions(+), 304 deletions(-) diff --git a/openspec/changes/add-nominal-unions/design.md b/openspec/changes/add-nominal-unions/design.md index b9cc7766c..a2f98f248 100644 --- a/openspec/changes/add-nominal-unions/design.md +++ b/openspec/changes/add-nominal-unions/design.md @@ -209,27 +209,35 @@ For each concrete parent application, target planning builds: NominalUnionLayout parent private tag representation - payload offset, size, alignment + stored carrier payload offset, size, alignment total size, alignment, padding variants[] variant identity, source ordinal, private tag - aggregate payload layout + canonical aggregate payload layout logical-field-to-fixed-slot calling mapping ``` Each field variant's payload uses the existing declaration-ordered struct field offset and padding algorithm, including concrete callable and Effect realizations. The enclosing payload uses the -maximum variant size and alignment. The private tag uses the existing deterministic private-tag -width policy and source-order ordinal; no source or external ABI observes it. +deterministic fixed carrier slots obtained by unifying every variant's logical calling lanes. Its +size and alignment cover those slots, but its offsets are compiler-owned and need not equal a +particular variant's struct-like offsets. The plan separately retains the maximum canonical payload +size and alignment needed for materialization. When a Drop hook or +other address-based operation needs the selected variant's fields, the backend materializes the +active carrier into canonical aggregate storage, performs the operation there, and writes any hook +mutation back through the same field-to-slot mapping before structural reclamation. The private tag +uses the existing deterministic private-tag width policy and source-order ordinal; no source or +external ABI observes it. Complete non-generic unions enter the nominal layout catalog before runtime reachability, including unavailable and unused private declarations. Open generics get no speculative layout. Reachable concrete generic applications receive canonical specialized entries. Mixed struct/union recursion is checked in one inline dependency graph, with explicit existing indirection as the only cycle break. -The calling shape is a tag lane plus fixed payload slots and a complete mapping from every variant's -logical aggregate lanes. MIR, evaluation, Wasm, and LLVM consume this one plan; backends do not infer -offsets, tag order, or call ABI independently. +The calling shape is a tag lane plus those same fixed payload slots and a complete mapping from every +variant's logical aggregate lanes. MIR, evaluation, Wasm, and LLVM consume this one plan; backends do +not infer offsets, tag order, or call ABI independently. Canonical aggregate offsets remain the +authority only while a selected variant is materialized for struct-like address semantics. ### 9. HIR and MIR use explicit nominal-union operations diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md index 0a8149d74..d97aeec02 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md @@ -6,15 +6,17 @@ Every complete non-generic nominal union SHALL receive a target-aware catalog en reachability, including unused private declarations. Every reachable concrete generic application SHALL receive one specialized entry, while an open generic declaration SHALL receive no speculative physical layout. Each available entry SHALL contain an inaccessible variant tag, one payload offset, -and storage aligned and sized for its largest concrete variant payload. Unit variants SHALL require -no payload bytes. The plan SHALL retain canonical parent, variant, field, ordinal, availability, -size, alignment, and padding metadata; source semantics SHALL expose no numeric tag, stable external -ABI, or serialization representation. +and a deterministic fixed carrier payload whose slots unify every variant's logical calling lanes. +The carrier SHALL be aligned and sized for all mapped lanes. Unit variants SHALL add no logical +payload lanes. The plan SHALL separately retain every concrete canonical variant payload layout and +the maximum materialization size and alignment. The plan SHALL retain canonical parent, +variant, field, ordinal, availability, size, alignment, and padding metadata; source semantics SHALL +expose no numeric tag, stable external ABI, or serialization representation. #### Scenario: Plan mixed unit and payload variants - **WHEN** a concrete union contains one unit variant and payload variants with distinct sizes and alignments -- **THEN** the layout contains one tag and one correctly aligned payload region sufficient for every variant with deterministic padding +- **THEN** the layout contains one tag and one correctly aligned fixed carrier region sufficient for every variant with deterministic padding #### Scenario: Specialize a generic union layout @@ -35,22 +37,27 @@ ABI, or serialization representation. Each named-field variant SHALL lay out its specialized fields in declaration order under the same target-aware offset, alignment, padding, represented-callable, represented-Effect, and unavailable- -dependency rules as a nominal struct. The enclosing union payload region SHALL satisfy the maximum -size and alignment of those complete variant payload layouts. Unit variants SHALL contribute an -empty payload layout and SHALL NOT create source-visible fields. +dependency rules as a nominal struct. The representation plan SHALL retain the maximum size and +alignment of those complete variant payload layouts for materialization while stored values use the +compiler-owned fixed carrier mapping rather than one variant's raw field offsets. An address-based +operation on the active payload SHALL materialize its fields at the canonical aggregate offsets; a +Drop hook's mutations SHALL be transferred back to the carrier before structural reclamation. Unit +variants SHALL contribute an empty payload layout and SHALL NOT create source-visible fields. #### Scenario: Lay out a padded multi-field variant - **WHEN** one variant contains multiple fields whose target alignments require internal and tail padding -- **THEN** its variant plan records the ordinary declaration-ordered field offsets and the union payload region preserves that complete aligned layout +- **THEN** its variant plan records the ordinary declaration-ordered field offsets and address-based operations observe that complete aligned layout after active-variant materialization ### Requirement: Nominal union calling shape is compiler-owned target data For every reachable nominal-union parameter or result, target planning SHALL publish one backend-neutral tag-plus-payload calling shape and a complete canonical mapping from every variant's logical field calling shape into fixed payload slots. Construction, calls, returns, matching, and -cleanup SHALL consume that same mapping. An unavailable variant layout or impossible mapping SHALL -make the calling shape unavailable before MIR or backend emission. +cleanup SHALL consume that same mapping. Cleanup requiring canonical field addresses SHALL use the +mapping in both directions rather than interpreting carrier offsets as variant field offsets. An +unavailable variant layout or impossible mapping SHALL make the calling shape unavailable before +MIR or backend emission. #### Scenario: Plan a nominal union call boundary diff --git a/openspec/changes/add-nominal-unions/tasks.md b/openspec/changes/add-nominal-unions/tasks.md index 5c69e3a76..5d515efef 100644 --- a/openspec/changes/add-nominal-unions/tasks.md +++ b/openspec/changes/add-nominal-unions/tasks.md @@ -45,7 +45,7 @@ ## 6. Target Layout and Calling Shapes - [x] 6.1 Extend the inline dependency graph and nominal layout catalog to include complete non-generic unions and mixed struct/union cycles, and verify unused private and unavailable union entries appear before runtime reachability. -- [x] 6.2 Add a distinct nominal-union representation plan with deterministic private tags, source-order ordinals, payload offset, maximum size/alignment, total padding, and per-variant aggregate layouts, and verify unit, padded multi-field, and `never` payload cases. +- [x] 6.2 Add a distinct nominal-union representation plan with deterministic private tags, source-order ordinals, a fixed unified carrier payload, total padding, complete per-variant canonical aggregate layouts, and bidirectional field mappings for address materialization, and verify unit, padded multi-field, cleanup, and `never` payload cases. - [x] 6.3 Specialize reachable generic union layouts without speculative open-generic entries, and verify equivalent concrete applications reuse one catalog identity while distinct applications receive distinct physical plans. - [x] 6.4 Publish a backend-neutral tag-plus-payload calling shape with complete per-variant logical-field mappings, and verify call/return plans for heterogeneous variants are deterministic and unavailable dependencies stop before MIR. - [x] 6.5 Extend layout encoding, verification, and Analysis projections with nominal-union facts under unambiguous internal names, and verify no nominal tag, padding, or ABI detail becomes source-observable. diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 97421c73b..1a10820e9 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '3f1662afaebdb546b979c13804418b262f67a6bde186ebb484ec7d3a164f9313' +export const compilerDigest = '7f07ce1e20db89da5e4eaf684a063db6835063da444a84fa1ac812a4e03bd086' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 4c860096d..9e47f59ac 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -1366,9 +1366,9 @@ const layoutOf = ( nextInternal += 2 } const internalCount = nextInternal - physical - const frameBase = frame.roots.size === 0 ? undefined : physical + internalCount - const frameEnd = frame.roots.size === 0 ? undefined : physical + internalCount + 1 - const framePages = frame.roots.size === 0 ? undefined : physical + internalCount + 2 + const frameBase = frame.size === 0 ? undefined : physical + internalCount + const frameEnd = frame.size === 0 ? undefined : physical + internalCount + 1 + const framePages = frame.size === 0 ? undefined : physical + internalCount + 2 if (frameBase !== undefined && frameEnd !== undefined && framePages !== undefined) { declared.push(named(i32, 'frame_base'), named(i32, 'frame_end'), named(i32, 'frame_pages')) } @@ -1728,86 +1728,154 @@ const makeOperationContext = ( if (environment !== undefined) return environment throw new RangeError('Wasm Effect cleanup lost its exact stored environment') } + interface AddressCleanupState { + readonly addressAt: (byteOffset: number) => ReadonlyArray + readonly byteOffset: number + readonly nominalDepth: number + } + const transferNominalUnionStorage = ( + cleanup: Extract, + carrierAddressAt: (byteOffset: number) => ReadonlyArray, + rawAddressAt: (byteOffset: number) => ReadonlyArray, + direction: 'CarrierToRaw' | 'RawToCarrier', + ): ReadonlyArray => { + if (memory === undefined) throw new RangeError('Wasm nominal union transfer has no memory') + const representation = LayoutPlan.entry(memory.plan, cleanup.type)?.representation + const shape = LayoutPlan.callingShape(memory.plan, cleanup.type) + if (representation?._tag !== 'NominalUnion' || shape?.tree._tag !== 'NominalUnionShape') { + throw new RangeError('Wasm nominal union transfer lost its layout') + } + const sourceAddressAt = direction === 'CarrierToRaw' ? carrierAddressAt : rawAddressAt + const targetAddressAt = direction === 'CarrierToRaw' ? rawAddressAt : carrierAddressAt + const variants = cleanup.variants.flatMap((variant) => { + const layoutVariant = representation.variants.find( + (candidate) => candidate.ordinal === variant.ordinal, + ) + if (layoutVariant === undefined) { + throw new RangeError('Wasm nominal union transfer lost its active variant layout') + } + const identity = Match.nominalUnionVariant( + cleanup.type, + cleanup.type, + variant.variant, + variant.ordinal, + ) + const transfers = layoutVariant.fields.flatMap((layoutField) => { + const physical = LayoutPlan.coverageFieldSlots(shape, identity, [layoutField.id]) + const fieldLanes = semanticLanesOf(layoutField.type) + if (physical === undefined || physical.length !== fieldLanes.length) { + throw new RangeError('Wasm nominal union transfer lost its complete field mapping') + } + return physical.flatMap((physicalOrdinal, fieldOrdinal) => { + const carrierLane = shape.lanes.at(physicalOrdinal) + const fieldLane = fieldLanes.at(fieldOrdinal) + const carrierOffset = + carrierLane === undefined + ? undefined + : LayoutVerify.laneOffset(memory.plan, cleanup.type, carrierLane.path) + const nestedOffset = + fieldLane === undefined + ? undefined + : LayoutVerify.laneOffset(memory.plan, layoutField.type, fieldLane.path) + if ( + carrierLane === undefined || + fieldLane === undefined || + carrierOffset === undefined || + nestedOffset === undefined + ) { + throw new RangeError('Wasm nominal union transfer lost a field lane') + } + const rawOffset = representation.payloadOffset + layoutField.offset + nestedOffset + const sourceLane = direction === 'CarrierToRaw' ? carrierLane : fieldLane + const targetLane = direction === 'CarrierToRaw' ? fieldLane : carrierLane + const sourceOffset = direction === 'CarrierToRaw' ? carrierOffset : rawOffset + const targetOffset = direction === 'CarrierToRaw' ? rawOffset : carrierOffset + return [ + ...targetAddressAt(targetOffset), + ...sourceAddressAt(sourceOffset), + Instr.memoryAccess(laneLoadMnemonic(memory.plan, sourceLane), memory.memory), + ...laneBridge( + laneValueType(memory.plan, sourceLane), + laneValueType(memory.plan, targetLane), + ), + Instr.memoryAccess(laneStoreMnemonic(memory.plan, targetLane), memory.memory), + ] + }) + }) + return [ + ...sourceAddressAt(0), + Instr.memoryAccess('i32.load', memory.memory), + Instr.i32Const(variant.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, transfers, []), + ] + }) + return [ + ...targetAddressAt(0), + ...sourceAddressAt(0), + Instr.memoryAccess('i32.load', memory.memory), + Instr.memoryAccess('i32.store', memory.memory), + ...variants, + ] + } const hookReleaseWalk = ( cleanup: CleanupPlan.CleanupPlan, addressAt: (byteOffset: number) => ReadonlyArray, ): ReadonlyArray => { if (!CleanupPlan.hasHook(cleanup)) return [] if (memory === undefined) throw new RangeError('Wasm hook cleanup has no private memory') - return WasmCleanup.emitCleanupWalk(cleanup, 0, (plan_, byteOffset) => { - switch (plan_._tag) { - case 'HookCleanup': - return Object.freeze({ - before: Object.freeze([ - ...addressAt(byteOffset), - Instr.call(resolve(plan_.hook, plan_.typeArguments)), - ]), - children: Object.freeze([Object.freeze({ cleanup: plan_.inner, state: byteOffset })]), - }) - case 'CallableCleanup': - return Object.freeze({ - children: Object.freeze( - plan_.slots.map((slot) => - Object.freeze({ - cleanup: slot.cleanup, - state: byteOffset + callableCaptureRange(plan_, slot.ordinal).byteOffset, + return WasmCleanup.emitCleanupWalk( + cleanup, + Object.freeze({ addressAt, byteOffset: 0, nominalDepth: 0 }), + (plan_, state) => { + const child = (nestedOffset: number): AddressCleanupState => + Object.freeze({ ...state, byteOffset: state.byteOffset + nestedOffset }) + switch (plan_._tag) { + case 'HookCleanup': + return Object.freeze({ + before: Object.freeze([ + ...state.addressAt(state.byteOffset), + Instr.call(resolve(plan_.hook, plan_.typeArguments)), + ]), + children: Object.freeze([Object.freeze({ cleanup: plan_.inner, state })]), + }) + case 'CallableCleanup': + return Object.freeze({ + children: Object.freeze( + plan_.slots.map((slot) => + Object.freeze({ + cleanup: slot.cleanup, + state: child(callableCaptureRange(plan_, slot.ordinal).byteOffset), + }), + ), + ), + }) + case 'EffectCleanup': { + const environment = effectEnvironmentForCleanup(plan_) + return Object.freeze({ + children: Object.freeze( + plan_.slots.flatMap((slot) => { + const field = environment.fields.at(slot.ordinal) + return field === undefined + ? [] + : [ + Object.freeze({ + cleanup: slot.cleanup, + state: child(field.offset), + }), + ] }), ), - ), - }) - case 'EffectCleanup': { - const environment = effectEnvironmentForCleanup(plan_) - return Object.freeze({ - children: Object.freeze( - plan_.slots.flatMap((slot) => { - const field = environment.fields.at(slot.ordinal) - return field === undefined - ? [] - : [ - Object.freeze({ - cleanup: slot.cleanup, - state: byteOffset + field.offset, - }), - ] - }), - ), - }) - } - case 'StructCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'Aggregate') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - plan_.fields.flatMap((field) => { - if (!CleanupPlan.hasHook(field.cleanup)) return [] - const layoutField = representation.fields.find((candidate) => - DeclarationFacts.sameFieldId(candidate.id, field.field), - ) - return layoutField === undefined - ? [] - : [ - Object.freeze({ - cleanup: field.cleanup, - state: byteOffset + layoutField.offset, - }), - ] - }), - ), - }) - } - case 'NominalUnionCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'NominalUnion') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - plan_.variants.flatMap((variant) => { - const layoutVariant = representation.variants.find( - (candidate) => candidate.ordinal === variant.ordinal, - ) - if (layoutVariant === undefined) return [] - return variant.fields.flatMap((field) => { + }) + } + case 'StructCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'Aggregate') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + plan_.fields.flatMap((field) => { if (!CleanupPlan.hasHook(field.cleanup)) return [] - const layoutField = layoutVariant.fields.find((candidate) => + const layoutField = representation.fields.find((candidate) => DeclarationFacts.sameFieldId(candidate.id, field.field), ) return layoutField === undefined @@ -1815,67 +1883,113 @@ const makeOperationContext = ( : [ Object.freeze({ cleanup: field.cleanup, - state: byteOffset + representation.payloadOffset + layoutField.offset, + state: child(layoutField.offset), + }), + ] + }), + ), + }) + } + case 'NominalUnionCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'NominalUnion') return Object.freeze({}) + const scratch = memory.frame.nominalUnionCleanupScratch.at(state.nominalDepth) + if (scratch === undefined) { + throw new RangeError('Wasm nominal union hook cleanup lost its canonical scratch') + } + const carrierAddressAt = (offset: number): ReadonlyArray => + state.addressAt(state.byteOffset + offset) + const rawAddressAt = (offset: number): ReadonlyArray => + frameAddress(scratch.offset + offset) + return Object.freeze({ + before: Object.freeze( + transferNominalUnionStorage(plan_, carrierAddressAt, rawAddressAt, 'CarrierToRaw'), + ), + children: Object.freeze( + plan_.variants.flatMap((variant) => { + const layoutVariant = representation.variants.find( + (candidate) => candidate.ordinal === variant.ordinal, + ) + if (layoutVariant === undefined) return [] + return variant.fields.flatMap((field) => { + if (!CleanupPlan.hasHook(field.cleanup)) return [] + const layoutField = layoutVariant.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), + ) + return layoutField === undefined + ? [] + : [ + Object.freeze({ + cleanup: field.cleanup, + state: Object.freeze({ + addressAt: rawAddressAt, + byteOffset: representation.payloadOffset + layoutField.offset, + nominalDepth: state.nominalDepth + 1, + }), + wrap: (instructions: ReadonlyArray) => + Object.freeze([ + ...rawAddressAt(0), + Instr.memoryAccess('i32.load', memory.memory), + Instr.i32Const(variant.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, instructions, []), + ]), + }), + ] + }) + }), + ), + after: Object.freeze( + transferNominalUnionStorage(plan_, carrierAddressAt, rawAddressAt, 'RawToCarrier'), + ), + }) + } + case 'ArrayCleanup': { + if (!CleanupPlan.hasHook(plan_.element)) return Object.freeze({}) + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'Repeated') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + Array.from({ length: plan_.length }, (_, index) => + Object.freeze({ + cleanup: plan_.element, + state: child(index * representation.stride), + }), + ), + ), + }) + } + case 'UnionCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'Union') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + plan_.cases.flatMap((caseEntry) => + CleanupPlan.hasHook(caseEntry.cleanup) + ? [ + Object.freeze({ + cleanup: caseEntry.cleanup, + state: child(representation.payloadOffset), wrap: (instructions: ReadonlyArray) => Object.freeze([ - ...addressAt(byteOffset), + ...state.addressAt(state.byteOffset), Instr.memoryAccess('i32.load', memory.memory), - Instr.i32Const(variant.ordinal), + Instr.i32Const(caseEntry.ordinal), Instr.op('i32.eq'), Instr.ifElse(Instr.emptyBlockType, instructions, []), ]), }), ] - }) - }), - ), - }) - } - case 'ArrayCleanup': { - if (!CleanupPlan.hasHook(plan_.element)) return Object.freeze({}) - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'Repeated') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - Array.from({ length: plan_.length }, (_, index) => - Object.freeze({ - cleanup: plan_.element, - state: byteOffset + index * representation.stride, - }), - ), - ), - }) - } - case 'UnionCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'Union') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - plan_.cases.flatMap((caseEntry) => - CleanupPlan.hasHook(caseEntry.cleanup) - ? [ - Object.freeze({ - cleanup: caseEntry.cleanup, - state: byteOffset + representation.payloadOffset, - wrap: (instructions: ReadonlyArray) => - Object.freeze([ - ...addressAt(byteOffset), - Instr.memoryAccess('i32.load', memory.memory), - Instr.i32Const(caseEntry.ordinal), - Instr.op('i32.eq'), - Instr.ifElse(Instr.emptyBlockType, instructions, []), - ]), - }), - ] - : [], + : [], + ), ), - ), - }) + }) + } + default: + return Object.freeze({}) } - default: - return Object.freeze({}) - } - }) + }, + ) } /** * Runs every Drop hook one cleanup plan invokes: the owner materializes to its frame root, @@ -2414,116 +2528,98 @@ const makeOperationContext = ( ): ReadonlyArray => { if (!CleanupPlan.reclaims(cleanup)) return [] if (memory === undefined) throw new RangeError('Wasm slot reclaim has no private memory') - return WasmCleanup.emitCleanupWalk(cleanup, byteOffset, (plan_, currentOffset) => { - if (!CleanupPlan.reclaims(plan_)) return Object.freeze({}) - switch (plan_._tag) { - case 'ExecutionCleanup': { - const scratch = layout.executionCleanupScratch.at(0) - if (scratch === undefined) { - throw new RangeError('Wasm Execution cleanup lost its package authority local') + const addressAt = (offset: number): ReadonlyArray => [ + Instr.localGet(address), + ...(offset === 0 ? [] : [Instr.i32Const(offset), Instr.op('i32.add')]), + ] + return WasmCleanup.emitCleanupWalk( + cleanup, + Object.freeze({ addressAt, byteOffset, nominalDepth: 0 }), + (plan_, state) => { + const child = (nestedOffset: number): AddressCleanupState => + Object.freeze({ ...state, byteOffset: state.byteOffset + nestedOffset }) + const load = (nestedOffset = 0): ReadonlyArray => [ + ...state.addressAt(state.byteOffset + nestedOffset), + Instr.memoryAccess('i32.load', memory.memory), + ] + if (!CleanupPlan.reclaims(plan_)) return Object.freeze({}) + switch (plan_._tag) { + case 'ExecutionCleanup': { + const scratch = layout.executionCleanupScratch.at(0) + if (scratch === undefined) { + throw new RangeError('Wasm Execution cleanup lost its package authority local') + } + return Object.freeze({ + before: Object.freeze([ + ...load(), + Instr.localSet(scratch.package), + ...releaseExecutionBase(scratch.package), + ]), + }) } - return Object.freeze({ - before: Object.freeze([ - ...loadAt(address, currentOffset), - Instr.localSet(scratch.package), - ...releaseExecutionBase(scratch.package), - ]), - }) - } - case 'WakeCleanup': { - const scratch = layout.executionCleanupScratch.at(0) - if (scratch === undefined) { - throw new RangeError('Wasm Wake cleanup lost its package authority local') + case 'WakeCleanup': { + const scratch = layout.executionCleanupScratch.at(0) + if (scratch === undefined) { + throw new RangeError('Wasm Wake cleanup lost its package authority local') + } + return Object.freeze({ + before: Object.freeze([ + ...load(), + Instr.localSet(scratch.package), + ...releaseWakeBase(scratch.package), + ]), + }) } - return Object.freeze({ - before: Object.freeze([ - ...loadAt(address, currentOffset), - Instr.localSet(scratch.package), - ...releaseWakeBase(scratch.package), - ]), - }) - } - case 'AllocationCleanup': - case 'RawBufferCleanup': { - const contextOffset = SilkType.isRawBuffer(plan_.type) - ? aggregateFieldOffset(plan_.type, '$allocation') + - aggregateFieldOffset(SilkType.allocation, '$context') - : aggregateFieldOffset(SilkType.allocation, '$context') - return Object.freeze({ - before: Object.freeze([ - ...loadAt(address, currentOffset + contextOffset), - Instr.call(requireRelease()), - ]), - }) - } - case 'HookCleanup': - return Object.freeze({ - children: Object.freeze([ - Object.freeze({ cleanup: plan_.inner, state: currentOffset }), - ]), - }) - case 'CallableCleanup': - return Object.freeze({ - children: Object.freeze( - plan_.slots.map((slot) => - Object.freeze({ - cleanup: slot.cleanup, - state: currentOffset + callableCaptureRange(plan_, slot.ordinal).byteOffset, + case 'AllocationCleanup': + case 'RawBufferCleanup': { + const contextOffset = SilkType.isRawBuffer(plan_.type) + ? aggregateFieldOffset(plan_.type, '$allocation') + + aggregateFieldOffset(SilkType.allocation, '$context') + : aggregateFieldOffset(SilkType.allocation, '$context') + return Object.freeze({ + before: Object.freeze([...load(contextOffset), Instr.call(requireRelease())]), + }) + } + case 'HookCleanup': + return Object.freeze({ + children: Object.freeze([Object.freeze({ cleanup: plan_.inner, state })]), + }) + case 'CallableCleanup': + return Object.freeze({ + children: Object.freeze( + plan_.slots.map((slot) => + Object.freeze({ + cleanup: slot.cleanup, + state: child(callableCaptureRange(plan_, slot.ordinal).byteOffset), + }), + ), + ), + }) + case 'EffectCleanup': { + const environment = effectEnvironmentForCleanup(plan_) + return Object.freeze({ + children: Object.freeze( + plan_.slots.flatMap((slot) => { + const field = environment.fields.at(slot.ordinal) + return field === undefined + ? [] + : [ + Object.freeze({ + cleanup: slot.cleanup, + state: child(field.offset), + }), + ] }), ), - ), - }) - case 'EffectCleanup': { - const environment = effectEnvironmentForCleanup(plan_) - return Object.freeze({ - children: Object.freeze( - plan_.slots.flatMap((slot) => { - const field = environment.fields.at(slot.ordinal) - return field === undefined - ? [] - : [ - Object.freeze({ - cleanup: slot.cleanup, - state: currentOffset + field.offset, - }), - ] - }), - ), - }) - } - case 'StructCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'Aggregate') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - plan_.fields.flatMap((field) => { - const layoutField = representation.fields.find((candidate) => - DeclarationFacts.sameFieldId(candidate.id, field.field), - ) - return layoutField === undefined - ? [] - : [ - Object.freeze({ - cleanup: field.cleanup, - state: currentOffset + layoutField.offset, - }), - ] - }), - ), - }) - } - case 'NominalUnionCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'NominalUnion') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - plan_.variants.flatMap((variant) => { - const layoutVariant = representation.variants.find( - (candidate) => candidate.ordinal === variant.ordinal, - ) - if (layoutVariant === undefined) return [] - return variant.fields.flatMap((field) => { - const layoutField = layoutVariant.fields.find((candidate) => + }) + } + case 'StructCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'Aggregate') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + plan_.fields.flatMap((field) => { + const layoutField = representation.fields.find((candidate) => DeclarationFacts.sameFieldId(candidate.id, field.field), ) return layoutField === undefined @@ -2531,64 +2627,107 @@ const makeOperationContext = ( : [ Object.freeze({ cleanup: field.cleanup, - state: currentOffset + representation.payloadOffset + layoutField.offset, - wrap: (instructions: ReadonlyArray) => - instructions.length === 0 - ? Object.freeze([]) - : Object.freeze([ - ...loadAt(address, currentOffset), - Instr.i32Const(variant.ordinal), - Instr.op('i32.eq'), - Instr.ifElse(Instr.emptyBlockType, instructions, []), - ]), + state: child(layoutField.offset), }), ] - }) - }), - ), - }) - } - case 'ArrayCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'Repeated') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - Array.from({ length: plan_.length }, (_, index) => - Object.freeze({ - cleanup: plan_.element, - state: currentOffset + index * representation.stride, }), ), - ), - }) - } - case 'UnionCleanup': { - const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation - if (representation?._tag !== 'Union') return Object.freeze({}) - return Object.freeze({ - children: Object.freeze( - plan_.cases.map((caseEntry) => - Object.freeze({ - cleanup: caseEntry.cleanup, - state: currentOffset + representation.payloadOffset, - wrap: (instructions: ReadonlyArray) => - instructions.length === 0 - ? Object.freeze([]) - : Object.freeze([ - ...loadAt(address, currentOffset), - Instr.i32Const(caseEntry.ordinal), - Instr.op('i32.eq'), - Instr.ifElse(Instr.emptyBlockType, instructions, []), - ]), + }) + } + case 'NominalUnionCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'NominalUnion') return Object.freeze({}) + const scratch = memory.frame.nominalUnionCleanupScratch.at(state.nominalDepth) + if (scratch === undefined) { + throw new RangeError('Wasm nominal union reclaim lost its canonical scratch') + } + const carrierAddressAt = (offset: number): ReadonlyArray => + state.addressAt(state.byteOffset + offset) + const rawAddressAt = (offset: number): ReadonlyArray => + frameAddress(scratch.offset + offset) + return Object.freeze({ + before: Object.freeze( + transferNominalUnionStorage(plan_, carrierAddressAt, rawAddressAt, 'CarrierToRaw'), + ), + children: Object.freeze( + plan_.variants.flatMap((variant) => { + const layoutVariant = representation.variants.find( + (candidate) => candidate.ordinal === variant.ordinal, + ) + if (layoutVariant === undefined) return [] + return variant.fields.flatMap((field) => { + const layoutField = layoutVariant.fields.find((candidate) => + DeclarationFacts.sameFieldId(candidate.id, field.field), + ) + return layoutField === undefined + ? [] + : [ + Object.freeze({ + cleanup: field.cleanup, + state: Object.freeze({ + addressAt: rawAddressAt, + byteOffset: representation.payloadOffset + layoutField.offset, + nominalDepth: state.nominalDepth + 1, + }), + wrap: (instructions: ReadonlyArray) => + instructions.length === 0 + ? Object.freeze([]) + : Object.freeze([ + ...rawAddressAt(0), + Instr.memoryAccess('i32.load', memory.memory), + Instr.i32Const(variant.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, instructions, []), + ]), + }), + ] + }) }), ), - ), - }) + }) + } + case 'ArrayCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'Repeated') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + Array.from({ length: plan_.length }, (_, index) => + Object.freeze({ + cleanup: plan_.element, + state: child(index * representation.stride), + }), + ), + ), + }) + } + case 'UnionCleanup': { + const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation + if (representation?._tag !== 'Union') return Object.freeze({}) + return Object.freeze({ + children: Object.freeze( + plan_.cases.map((caseEntry) => + Object.freeze({ + cleanup: caseEntry.cleanup, + state: child(representation.payloadOffset), + wrap: (instructions: ReadonlyArray) => + instructions.length === 0 + ? Object.freeze([]) + : Object.freeze([ + ...load(), + Instr.i32Const(caseEntry.ordinal), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, instructions, []), + ]), + }), + ), + ), + }) + } + default: + return Object.freeze({}) } - default: - return Object.freeze({}) - } - }) + }, + ) } /** * One owned value's complete release: its Drop hooks, then the blocks its reclaim tickets still @@ -7550,13 +7689,13 @@ const emitBody = ( return first } const restoreFrame = (): ReadonlyArray => - memory === undefined || memory.frame.roots.size === 0 || layout.frameBase === undefined + memory === undefined || memory.frame.size === 0 || layout.frameBase === undefined ? [] : [Instr.localGet(layout.frameBase), Instr.globalSet(memory.stackPointer)] const reserveFrame = (): ReadonlyArray => { if ( memory === undefined || - memory.frame.roots.size === 0 || + memory.frame.size === 0 || layout.frameBase === undefined || layout.frameEnd === undefined || layout.framePages === undefined diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 16465bb66..975fc82d5 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -28,10 +28,101 @@ export interface FramePlan { readonly escaping: ReadonlySet /** Address-taken frame roots reachable through each MIR local's stored pointer lanes. */ readonly localRoots: ReadonlyMap> + /** Depth-indexed canonical variant storage used while cleaning widened nominal-union carriers. */ + readonly nominalUnionCleanupScratch: ReadonlyArray<{ + readonly offset: number + readonly size: number + readonly alignment: number + }> readonly size: number readonly alignment: number } +const operationCleanupPlans = ( + operation: Mir.Operation, +): ReadonlyArray => { + switch (operation._tag) { + case 'SlotDrop': + case 'Drop': + return Object.freeze([operation.cleanup]) + case 'SharedWithMut': + return Object.freeze([operation.useCleanup, operation.conflictCleanup]) + case 'ExecutionPark': + return Object.freeze([operation.guardCleanup, operation.registerCleanup]) + case 'PropagateEffectFailure': + case 'RunEffect': + case 'RunEffectValue': + case 'RunEffectComposite': + case 'RunStaticEffect': + return Object.freeze((operation.releases ?? []).map((release) => release.cleanup)) + case 'CloseEffectEntry': + return Object.freeze(operation.failures.map((failure) => failure.cleanup)) + case 'Match': + return Object.freeze( + operation.arms.flatMap((arm) => arm.selected.cleanup.map((entry) => entry.cleanup)), + ) + default: + return Object.freeze([]) + } +} + +const nominalUnionScratchRequirements = ( + cleanups: ReadonlyArray, + plan: LayoutPlan.Plan, +): ReadonlyArray<{ readonly size: number; readonly alignment: number }> => { + const requirements: Array<{ size: number; alignment: number }> = [] + const visit = (cleanup: CleanupPlan.CleanupPlan, depth: number): void => { + switch (cleanup._tag) { + case 'HookCleanup': + visit(cleanup.inner, depth) + return + case 'StructCleanup': + for (const field of cleanup.fields) visit(field.cleanup, depth) + return + case 'NominalUnionCleanup': { + const entry = LayoutPlan.entry(plan, cleanup.type) + if (entry?.representation._tag !== 'NominalUnion') return + const payloadSize = entry.representation.variants.reduce( + (maximum, variant) => Math.max(maximum, variant.size), + 0, + ) + const size = alignUp(entry.representation.payloadOffset + payloadSize, entry.alignment) + const current = requirements.at(depth) + requirements[depth] = { + size: Math.max(current?.size ?? 0, size), + alignment: Math.max(current?.alignment ?? 1, entry.alignment), + } + for (const variant of cleanup.variants) + for (const field of variant.fields) visit(field.cleanup, depth + 1) + return + } + case 'ArrayCleanup': + visit(cleanup.element, depth) + return + case 'UnionCleanup': + for (const entry of cleanup.cases) visit(entry.cleanup, depth) + return + case 'CallableCleanup': + case 'EffectCleanup': + for (const slot of cleanup.slots) visit(slot.cleanup, depth) + return + case 'EffectCompositeCleanup': + for (const alternative of cleanup.alternatives) visit(alternative, depth) + return + case 'RawBufferCleanup': + case 'LocalSharedCoreCleanup': + case 'ExecutionCleanup': + case 'WakeCleanup': + visit(cleanup.allocation, depth) + return + default: + return + } + } + for (const cleanup of cleanups) visit(cleanup, 0) + return Object.freeze(requirements.map((requirement) => Object.freeze(requirement))) +} + export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan => { const formations = MirVerification.operations(fn).filter( (operation): operation is Extract => @@ -264,6 +355,16 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan } } } + const nominalUnionCleanupScratch = nominalUnionScratchRequirements( + operations.flatMap(operationCleanupPlans), + plan, + ).map((requirement) => { + cursor = alignUp(cursor, requirement.alignment) + const scratch = Object.freeze({ ...requirement, offset: cursor }) + cursor += requirement.size + alignment = Math.max(alignment, requirement.alignment) + return scratch + }) const frozenLocalRoots = new Map( [...localRoots].map(([local, reachable]) => [local, new Set(reachable)] as const), ) @@ -271,6 +372,7 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan roots, escaping, localRoots: frozenLocalRoots, + nominalUnionCleanupScratch: Object.freeze(nominalUnionCleanupScratch), size: alignUp(cursor, alignment), alignment, }) diff --git a/packages/compiler/test/SlotLaneWidth.test.ts b/packages/compiler/test/SlotLaneWidth.test.ts index e9bff5246..210e47c03 100644 --- a/packages/compiler/test/SlotLaneWidth.test.ts +++ b/packages/compiler/test/SlotLaneWidth.test.ts @@ -207,6 +207,73 @@ pub fn main() -> i32 { return run Effect.catchAll(store(), recover) }` }), ) +it.effect('materializes canonical nominal-union fields before address-based Drop cleanup', () => + Effect.gen(function* () { + const source = `import silk.allocator { Allocator } +import silk.allocator { OutOfMemoryError } +import silk.allocator { SystemAllocator } +import silk.effect as Effect +import silk.i8 as i8 +import silk.layout { Layout } +import silk.raw_buffer as RawBuffer +import silk.slot as Slot + +struct Guard { + left: i8 + right: i8 +} + +impl Drop for Guard { + fn drop(self: &mut Guard) -> () { + let observed = i8.toI32(self.left) + i8.toI32(self.right) + if observed != 42 { + let boom = 1 / 0 + } + return () + } +} + +union Choice { + Small { marker: i8, guard: Guard }, + Wide { value: i64 }, +} + +effect fn store() -> i32 ! OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + let layout = Layout.of<[Choice; 1]>() + let recipe = Allocator.allocate(move layout) |> Effect.provideMut(&mut allocator) + let allocation = run recipe + unsafe { + let mut buffer = RawBuffer.from(move allocation, 1) + let value = Choice.Small { marker: 7, guard: Guard { left: 19, right: 23 } } + let written = Slot.write(RawBuffer.slot(&mut buffer, 0), move value) + let cleared = Slot.dropValue(RawBuffer.slot(&mut buffer, 0)) + drop buffer + return 42 + } + return 0 +} + +effect fn recover(error: OutOfMemoryError) -> i32 { return 0 } +pub fn main() -> i32 { return run Effect.catchAll(store(), recover) }` + const snapshot = yield* Analysis.ofSourceRealized( + 'slot-lane-width/nominal-union-cleanup', + ascii(source), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual(evaluated._tag, 'Completed') + if (evaluated._tag !== 'Completed') return + assert.strictEqual(evaluated.result.value, 42n) + + const wasm = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + /** * Reading a field through a reference lands in the same lane-load helper the slot reads use, so * a struct packed with sub-word fields catches a fixed-width load there the same way: every From d97a7c3ae4d44e603c32a6d1e0dd4bd30d3bae5d Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:43:01 -0300 Subject: [PATCH 39/42] fix(wasm): retain zero-sized address frames --- packages/compiler/src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 1a10820e9..16c00e7de 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '7f07ce1e20db89da5e4eaf684a063db6835063da444a84fa1ac812a4e03bd086' +export const compilerDigest = 'f13e541e349f7a5e83cf7e6567dcb90e1df5f26ce2a5d18928ebe64056af1c67' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 9e47f59ac..1880a1964 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -1366,9 +1366,10 @@ const layoutOf = ( nextInternal += 2 } const internalCount = nextInternal - physical - const frameBase = frame.size === 0 ? undefined : physical + internalCount - const frameEnd = frame.size === 0 ? undefined : physical + internalCount + 1 - const framePages = frame.size === 0 ? undefined : physical + internalCount + 2 + const needsFrame = frame.roots.size !== 0 || frame.nominalUnionCleanupScratch.length !== 0 + const frameBase = needsFrame ? physical + internalCount : undefined + const frameEnd = needsFrame ? physical + internalCount + 1 : undefined + const framePages = needsFrame ? physical + internalCount + 2 : undefined if (frameBase !== undefined && frameEnd !== undefined && framePages !== undefined) { declared.push(named(i32, 'frame_base'), named(i32, 'frame_end'), named(i32, 'frame_pages')) } @@ -7689,13 +7690,12 @@ const emitBody = ( return first } const restoreFrame = (): ReadonlyArray => - memory === undefined || memory.frame.size === 0 || layout.frameBase === undefined + memory === undefined || layout.frameBase === undefined ? [] : [Instr.localGet(layout.frameBase), Instr.globalSet(memory.stackPointer)] const reserveFrame = (): ReadonlyArray => { if ( memory === undefined || - memory.frame.size === 0 || layout.frameBase === undefined || layout.frameEnd === undefined || layout.framePages === undefined From ca0a264f7c8b164c5005b6f7ed8a10e1b71545fb Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:47:08 -0300 Subject: [PATCH 40/42] refactor(layout): derive union materialization --- openspec/changes/add-nominal-unions/design.md | 3 +- .../specs/bootstrap-target-layout/spec.md | 5 +-- packages/compiler/src/Layout.ts | 31 +++++++++++++++++++ .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 9 ++++-- packages/compiler/src/WasmMemory.ts | 10 ++---- 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/openspec/changes/add-nominal-unions/design.md b/openspec/changes/add-nominal-unions/design.md index a2f98f248..2b04097f4 100644 --- a/openspec/changes/add-nominal-unions/design.md +++ b/openspec/changes/add-nominal-unions/design.md @@ -222,7 +222,8 @@ algorithm, including concrete callable and Effect realizations. The enclosing pa deterministic fixed carrier slots obtained by unifying every variant's logical calling lanes. Its size and alignment cover those slots, but its offsets are compiler-owned and need not equal a particular variant's struct-like offsets. The plan separately retains the maximum canonical payload -size and alignment needed for materialization. When a Drop hook or +size and alignment needed for materialization as deterministic derivations of the complete variant +layouts. When a Drop hook or other address-based operation needs the selected variant's fields, the backend materializes the active carrier into canonical aggregate storage, performs the operation there, and writes any hook mutation back through the same field-to-slot mapping before structural reclamation. The private tag diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md index d97aeec02..e1ed6a468 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-target-layout/spec.md @@ -9,7 +9,8 @@ physical layout. Each available entry SHALL contain an inaccessible variant tag, and a deterministic fixed carrier payload whose slots unify every variant's logical calling lanes. The carrier SHALL be aligned and sized for all mapped lanes. Unit variants SHALL add no logical payload lanes. The plan SHALL separately retain every concrete canonical variant payload layout and -the maximum materialization size and alignment. The plan SHALL retain canonical parent, +SHALL deterministically derive the maximum materialization size and alignment from those layouts. +The plan SHALL retain canonical parent, variant, field, ordinal, availability, size, alignment, and padding metadata; source semantics SHALL expose no numeric tag, stable external ABI, or serialization representation. @@ -37,7 +38,7 @@ expose no numeric tag, stable external ABI, or serialization representation. Each named-field variant SHALL lay out its specialized fields in declaration order under the same target-aware offset, alignment, padding, represented-callable, represented-Effect, and unavailable- -dependency rules as a nominal struct. The representation plan SHALL retain the maximum size and +dependency rules as a nominal struct. The representation plan SHALL derive the maximum size and alignment of those complete variant payload layouts for materialization while stored values use the compiler-owned fixed carrier mapping rather than one variant's raw field offsets. An address-based operation on the active payload SHALL materialize its fields at the canonical aggregate offsets; a diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index 3200981a8..9f096d95a 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -171,6 +171,37 @@ export type Representation = readonly cleanupHook?: CleanupHook } +/** Canonical struct-like storage used transiently for one selected nominal-union variant. */ +export interface NominalUnionMaterialization { + readonly payloadOffset: number + readonly payloadSize: number + readonly payloadAlignment: number + readonly size: number + readonly alignment: number +} + +export const nominalUnionMaterialization = ( + representation: Extract, +): NominalUnionMaterialization => { + const payloadSize = representation.variants.reduce( + (maximum, variant) => Math.max(maximum, variant.size), + 0, + ) + const payloadAlignment = representation.variants.reduce( + (maximum, variant) => Math.max(maximum, variant.alignment), + 1, + ) + const payloadOffset = alignUp(4, payloadAlignment) + const alignment = Math.max(4, payloadAlignment) + return Object.freeze({ + payloadOffset, + payloadSize, + payloadAlignment, + size: alignUp(payloadOffset + payloadSize, alignment), + alignment, + }) +} + /** One compiler-owned concrete layout entry. */ export interface Entry { readonly _tag: 'LayoutEntry' diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 16c00e7de..7c3040a47 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'f13e541e349f7a5e83cf7e6567dcb90e1df5f26ce2a5d18928ebe64056af1c67' +export const compilerDigest = 'e0155087bb02044e314a7ac354550885a7e00e4aacf97ac5d3d0527f4406d46e' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 1880a1964..952dfdbba 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -1746,6 +1746,7 @@ const makeOperationContext = ( if (representation?._tag !== 'NominalUnion' || shape?.tree._tag !== 'NominalUnionShape') { throw new RangeError('Wasm nominal union transfer lost its layout') } + const materialization = LayoutPlan.nominalUnionMaterialization(representation) const sourceAddressAt = direction === 'CarrierToRaw' ? carrierAddressAt : rawAddressAt const targetAddressAt = direction === 'CarrierToRaw' ? rawAddressAt : carrierAddressAt const variants = cleanup.variants.flatMap((variant) => { @@ -1786,7 +1787,7 @@ const makeOperationContext = ( ) { throw new RangeError('Wasm nominal union transfer lost a field lane') } - const rawOffset = representation.payloadOffset + layoutField.offset + nestedOffset + const rawOffset = materialization.payloadOffset + layoutField.offset + nestedOffset const sourceLane = direction === 'CarrierToRaw' ? carrierLane : fieldLane const targetLane = direction === 'CarrierToRaw' ? fieldLane : carrierLane const sourceOffset = direction === 'CarrierToRaw' ? carrierOffset : rawOffset @@ -1894,6 +1895,7 @@ const makeOperationContext = ( case 'NominalUnionCleanup': { const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation if (representation?._tag !== 'NominalUnion') return Object.freeze({}) + const materialization = LayoutPlan.nominalUnionMaterialization(representation) const scratch = memory.frame.nominalUnionCleanupScratch.at(state.nominalDepth) if (scratch === undefined) { throw new RangeError('Wasm nominal union hook cleanup lost its canonical scratch') @@ -1924,7 +1926,7 @@ const makeOperationContext = ( cleanup: field.cleanup, state: Object.freeze({ addressAt: rawAddressAt, - byteOffset: representation.payloadOffset + layoutField.offset, + byteOffset: materialization.payloadOffset + layoutField.offset, nominalDepth: state.nominalDepth + 1, }), wrap: (instructions: ReadonlyArray) => @@ -2638,6 +2640,7 @@ const makeOperationContext = ( case 'NominalUnionCleanup': { const representation = LayoutPlan.entry(memory.plan, plan_.type)?.representation if (representation?._tag !== 'NominalUnion') return Object.freeze({}) + const materialization = LayoutPlan.nominalUnionMaterialization(representation) const scratch = memory.frame.nominalUnionCleanupScratch.at(state.nominalDepth) if (scratch === undefined) { throw new RangeError('Wasm nominal union reclaim lost its canonical scratch') @@ -2667,7 +2670,7 @@ const makeOperationContext = ( cleanup: field.cleanup, state: Object.freeze({ addressAt: rawAddressAt, - byteOffset: representation.payloadOffset + layoutField.offset, + byteOffset: materialization.payloadOffset + layoutField.offset, nominalDepth: state.nominalDepth + 1, }), wrap: (instructions: ReadonlyArray) => diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 975fc82d5..62bec0ebf 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -82,15 +82,11 @@ const nominalUnionScratchRequirements = ( case 'NominalUnionCleanup': { const entry = LayoutPlan.entry(plan, cleanup.type) if (entry?.representation._tag !== 'NominalUnion') return - const payloadSize = entry.representation.variants.reduce( - (maximum, variant) => Math.max(maximum, variant.size), - 0, - ) - const size = alignUp(entry.representation.payloadOffset + payloadSize, entry.alignment) + const materialization = LayoutPlan.nominalUnionMaterialization(entry.representation) const current = requirements.at(depth) requirements[depth] = { - size: Math.max(current?.size ?? 0, size), - alignment: Math.max(current?.alignment ?? 1, entry.alignment), + size: Math.max(current?.size ?? 0, materialization.size), + alignment: Math.max(current?.alignment ?? 1, materialization.alignment), } for (const variant of cleanup.variants) for (const field of variant.fields) visit(field.cleanup, depth + 1) From e1446e56e7bdf3937c2345fca7298fb777d04360 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 22:57:24 -0300 Subject: [PATCH 41/42] fix(wasm): plan synthetic cleanup frames --- .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 217 ++++++++---------- packages/compiler/src/WasmMemory.ts | 126 +++++----- .../compiler/test/ExecutionPackage.test.ts | 71 ++++++ packages/compiler/test/IntegerScalars.test.ts | 80 +++++++ 5 files changed, 313 insertions(+), 183 deletions(-) diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 7c3040a47..85e741d2d 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'e0155087bb02044e314a7ac354550885a7e00e4aacf97ac5d3d0527f4406d46e' +export const compilerDigest = 'b35797a2b76e1a18130a114b917538fc55a8bcff9cd66d1f771e5d29e6be4b57' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 952dfdbba..cf2317d82 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -56,6 +56,7 @@ import { heapBase, heapHeaderBytes, heapReleaseBody, + operationCleanupPlans, stackLimit, } from './WasmMemory.js' import * as WasmSuspension from './WasmSuspension.js' @@ -912,34 +913,6 @@ const localSharedCleanupDepth = (cleanup: CleanupPlan.CleanupPlan): number => { } } -const operationCleanupPlans = ( - operation: Mir.Operation, -): ReadonlyArray => { - switch (operation._tag) { - case 'SlotDrop': - case 'Drop': - return Object.freeze([operation.cleanup]) - case 'SharedWithMut': - return Object.freeze([operation.useCleanup, operation.conflictCleanup]) - case 'ExecutionPark': - return Object.freeze([operation.guardCleanup, operation.registerCleanup]) - case 'PropagateEffectFailure': - case 'RunEffect': - case 'RunEffectValue': - case 'RunEffectComposite': - case 'RunStaticEffect': - return Object.freeze((operation.releases ?? []).map((release) => release.cleanup)) - case 'CloseEffectEntry': - return Object.freeze(operation.failures.map((failure) => failure.cleanup)) - case 'Match': - return Object.freeze( - operation.arms.flatMap((arm) => arm.selected.cleanup.map((entry) => entry.cleanup)), - ) - default: - return Object.freeze([]) - } -} - const containsExecutionCleanup = (cleanup: CleanupPlan.CleanupPlan): boolean => { switch (cleanup._tag) { case 'ExecutionCleanup': @@ -7666,6 +7639,86 @@ const branchDepth = ( return depth } +const restoreInvocationFrame = ( + layout: Layout, + memory: MemoryContext | undefined, +): ReadonlyArray => + memory === undefined || layout.frameBase === undefined + ? [] + : [Instr.localGet(layout.frameBase), Instr.globalSet(memory.stackPointer)] + +const reserveInvocationFrame = ( + layout: Layout, + memory: MemoryContext | undefined, +): ReadonlyArray => { + if ( + memory === undefined || + layout.frameBase === undefined || + layout.frameEnd === undefined || + layout.framePages === undefined + ) { + return [] + } + if (memory.frame.size === 0) { + return [Instr.globalGet(memory.stackPointer), Instr.localSet(layout.frameBase)] + } + const report = (reason: number): ReadonlyArray => [ + Instr.i32Const(statusAddress), + Instr.i32Const(reason), + Instr.memoryAccess('i32.store', memory.memory), + Instr.i32Const(memory.stackBase), + Instr.globalSet(memory.stackPointer), + Instr.op('unreachable'), + ] + const boundCheck = + memory.stackLimit === undefined + ? [] + : [ + Instr.localGet(layout.frameEnd), + Instr.i32Const(memory.stackLimit), + Instr.op('i32.gt_u'), + Instr.ifElse(Instr.emptyBlockType, report(statusStackOverflow), []), + ] + return [ + Instr.globalGet(memory.stackPointer), + Instr.localSet(layout.frameBase), + Instr.localGet(layout.frameBase), + Instr.i32Const(memory.frame.size), + Instr.op('i32.add'), + Instr.localTee(layout.frameEnd), + Instr.localGet(layout.frameBase), + Instr.op('i32.lt_u'), + Instr.ifElse(Instr.emptyBlockType, [Instr.op('unreachable')], []), + ...boundCheck, + Instr.localGet(layout.frameEnd), + Instr.i32Const(1), + Instr.op('i32.sub'), + Instr.i32Const(16), + Instr.op('i32.shr_u'), + Instr.i32Const(1), + Instr.op('i32.add'), + Instr.localSet(layout.framePages), + Instr.localGet(layout.framePages), + Instr.memorySize(memory.memory), + Instr.op('i32.gt_u'), + Instr.ifElse( + Instr.emptyBlockType, + [ + Instr.localGet(layout.framePages), + Instr.memorySize(memory.memory), + Instr.op('i32.sub'), + Instr.memoryGrow(memory.memory), + Instr.i32Const(-1), + Instr.op('i32.eq'), + Instr.ifElse(Instr.emptyBlockType, [Instr.op('unreachable')], []), + ], + [], + ), + Instr.localGet(layout.frameEnd), + Instr.globalSet(memory.stackPointer), + ] +} + /** Direct structured emission from canonical regions; no CFG recovery or dispatch loop exists. */ const emitBody = ( context: WasmEmitContext.WasmEmitContext, @@ -7692,90 +7745,8 @@ const emitBody = ( } return first } - const restoreFrame = (): ReadonlyArray => - memory === undefined || layout.frameBase === undefined - ? [] - : [Instr.localGet(layout.frameBase), Instr.globalSet(memory.stackPointer)] - const reserveFrame = (): ReadonlyArray => { - if ( - memory === undefined || - layout.frameBase === undefined || - layout.frameEnd === undefined || - layout.framePages === undefined - ) { - return [] - } - if (memory.frame.size === 0) { - return [Instr.globalGet(memory.stackPointer), Instr.localSet(layout.frameBase)] - } - /** - * Report a deliberate trap rather than just taking one: name the reason in the status word, and - * rewind the stack pointer to the base so the trap is a single legible event instead of a - * module that answers every later call with the same trap. Whatever the abandoned frames owned - * is leaked — no cleanup ran — but the allocator's own structures are untouched, so a host that - * catches this can still read the heap back and get true answers out of it. - */ - const report = (reason: number): ReadonlyArray => [ - Instr.i32Const(statusAddress), - Instr.i32Const(reason), - Instr.memoryAccess('i32.store', memory.memory), - Instr.i32Const(memory.stackBase), - Instr.globalSet(memory.stackPointer), - Instr.op('unreachable'), - ] - /** - * One comparison against an address known at emission, on the path that already computed - * `frameEnd`. A reservation that would cross into the heap reports here, before the stack - * pointer moves, instead of being noticed downstream as corrupted memory. - */ - const boundCheck = - memory.stackLimit === undefined - ? [] - : [ - Instr.localGet(layout.frameEnd), - Instr.i32Const(memory.stackLimit), - Instr.op('i32.gt_u'), - Instr.ifElse(Instr.emptyBlockType, report(statusStackOverflow), []), - ] - return [ - Instr.globalGet(memory.stackPointer), - Instr.localSet(layout.frameBase), - Instr.localGet(layout.frameBase), - Instr.i32Const(memory.frame.size), - Instr.op('i32.add'), - Instr.localTee(layout.frameEnd), - Instr.localGet(layout.frameBase), - Instr.op('i32.lt_u'), - Instr.ifElse(Instr.emptyBlockType, [Instr.op('unreachable')], []), - ...boundCheck, - Instr.localGet(layout.frameEnd), - Instr.i32Const(1), - Instr.op('i32.sub'), - Instr.i32Const(16), - Instr.op('i32.shr_u'), - Instr.i32Const(1), - Instr.op('i32.add'), - Instr.localSet(layout.framePages), - Instr.localGet(layout.framePages), - Instr.memorySize(memory.memory), - Instr.op('i32.gt_u'), - Instr.ifElse( - Instr.emptyBlockType, - [ - Instr.localGet(layout.framePages), - Instr.memorySize(memory.memory), - Instr.op('i32.sub'), - Instr.memoryGrow(memory.memory), - Instr.i32Const(-1), - Instr.op('i32.eq'), - Instr.ifElse(Instr.emptyBlockType, [Instr.op('unreachable')], []), - ], - [], - ), - Instr.localGet(layout.frameEnd), - Instr.globalSet(memory.stackPointer), - ] - } + const restoreFrame = (): ReadonlyArray => restoreInvocationFrame(layout, memory) + const reserveFrame = (): ReadonlyArray => reserveInvocationFrame(layout, memory) const loadBorrowedParameters = (): ReadonlyArray => { if (layout.borrowPointers.size === 0) return [] if (memory === undefined) throw new RangeError('Wasm Effect borrow has no private memory') @@ -9047,7 +9018,15 @@ const emitProgramUnmapped = Effect.fnUntraced(function* ( }), ]), }) - const helperFrame = framePlan(helperFunction, program.layout) + const helperFrame = framePlan( + helperFunction, + program.layout, + [...executionPackageCleanups.values()].flatMap((packageCleanup) => [ + packageCleanup.body, + packageCleanup.endpoint, + packageCleanup.callback, + ]), + ) const helperLayout = layoutOf(helperFunction, program.layout, helperFrame, debug, false) const helperMemory: MemoryContext = Object.freeze({ memory: privateMemory, @@ -9079,7 +9058,11 @@ const emitProgramUnmapped = Effect.fnUntraced(function* ( ) yield* FuncActor.define(builder, executionCleanupHelper, { locals: helperLayout.declared, - body: operation.releaseExecutionBaseInline(0), + body: [ + ...reserveInvocationFrame(helperLayout, helperMemory), + ...operation.releaseExecutionBaseInline(0), + ...restoreInvocationFrame(helperLayout, helperMemory), + ], }) } @@ -9192,7 +9175,11 @@ const emitProgramUnmapped = Effect.fnUntraced(function* ( }) yield* FuncActor.define(builder, handle, { locals: cleanupLayout.declared, - body, + body: [ + ...reserveInvocationFrame(cleanupLayout, memory), + ...body, + ...restoreInvocationFrame(cleanupLayout, memory), + ], }) } diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 62bec0ebf..1b20af4b3 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -38,34 +38,68 @@ export interface FramePlan { readonly alignment: number } -const operationCleanupPlans = ( +export interface OperationCleanupEntry { + readonly cleanup: CleanupPlan.CleanupPlan + readonly local?: Mir.LocalId +} + +export const operationCleanupEntries = ( operation: Mir.Operation, -): ReadonlyArray => { +): ReadonlyArray => { switch (operation._tag) { - case 'SlotDrop': case 'Drop': - return Object.freeze([operation.cleanup]) + return Object.freeze([Object.freeze({ cleanup: operation.cleanup, local: operation.local })]) + case 'SlotDrop': + return Object.freeze([Object.freeze({ cleanup: operation.cleanup })]) + case 'CheckedScalar': + return Object.freeze([ + Object.freeze({ cleanup: operation.presentCleanup, local: operation.present }), + Object.freeze({ cleanup: operation.absentCleanup, local: operation.absent }), + ]) case 'SharedWithMut': - return Object.freeze([operation.useCleanup, operation.conflictCleanup]) + return Object.freeze([ + Object.freeze({ cleanup: operation.useCleanup, local: operation.use }), + Object.freeze({ cleanup: operation.conflictCleanup, local: operation.onConflict }), + ]) case 'ExecutionPark': - return Object.freeze([operation.guardCleanup, operation.registerCleanup]) + return Object.freeze([ + Object.freeze({ cleanup: operation.guardCleanup, local: operation.guard }), + Object.freeze({ cleanup: operation.registerCleanup, local: operation.register }), + ]) case 'PropagateEffectFailure': case 'RunEffect': case 'RunEffectValue': case 'RunEffectComposite': case 'RunStaticEffect': - return Object.freeze((operation.releases ?? []).map((release) => release.cleanup)) + return Object.freeze( + (operation.releases ?? []).map((release) => + Object.freeze({ cleanup: release.cleanup, local: release.local }), + ), + ) case 'CloseEffectEntry': - return Object.freeze(operation.failures.map((failure) => failure.cleanup)) + return Object.freeze( + operation.failures.map((failure) => + Object.freeze({ cleanup: failure.cleanup, local: failure.payload }), + ), + ) case 'Match': return Object.freeze( - operation.arms.flatMap((arm) => arm.selected.cleanup.map((entry) => entry.cleanup)), + operation.arms.flatMap((arm) => + arm.selected.cleanup.map((entry) => + Object.freeze({ cleanup: entry.cleanup, local: entry.destination }), + ), + ), ) default: return Object.freeze([]) } } +export const operationCleanupPlans = ( + operation: Mir.Operation, +): ReadonlyArray => + Object.freeze(operationCleanupEntries(operation).map((entry) => entry.cleanup)) + const nominalUnionScratchRequirements = ( cleanups: ReadonlyArray, plan: LayoutPlan.Plan, @@ -119,7 +153,11 @@ const nominalUnionScratchRequirements = ( return Object.freeze(requirements.map((requirement) => Object.freeze(requirement))) } -export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan => { +export const framePlan = ( + fn: Mir.MirFunction, + plan: LayoutPlan.Plan, + additionalCleanups: ReadonlyArray = Object.freeze([]), +): FramePlan => { const formations = MirVerification.operations(fn).filter( (operation): operation is Extract => operation._tag === 'BeginLoan', @@ -147,62 +185,16 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan ]) const rootOrdinals = new Set([ ...escaping, - // A hook-bearing drop passes `&mut self` into its hook, so the owner needs frame storage. - ...MirVerification.operations(fn).flatMap((operation) => { - const droppedOrdinals: Array = [] - if (operation._tag === 'Drop') { - if ( - CleanupPlan.hasHook(operation.cleanup) && - fn.localTypes.at(operation.local.ordinal)?._tag !== 'EffectBorrow' - ) { - droppedOrdinals.push(operation.local.ordinal) - } - } else if ( - operation._tag === 'RunEffect' || - operation._tag === 'RunEffectValue' || - operation._tag === 'RunEffectComposite' || - operation._tag === 'RunStaticEffect' - ) { - for (const release of operation.releases ?? []) { - if ( - CleanupPlan.hasHook(release.cleanup) && - fn.localTypes.at(release.local.ordinal)?._tag !== 'EffectBorrow' - ) { - droppedOrdinals.push(release.local.ordinal) - } - } - } - return [ - ...droppedOrdinals, - ...(operation._tag === 'CloseEffectEntry' - ? operation.failures.flatMap((failure) => - CleanupPlan.hasHook(failure.cleanup) ? [failure.payload.ordinal] : [], - ) - : []), - ...(operation._tag === 'SharedWithMut' - ? [ - ...(CleanupPlan.hasHook(operation.useCleanup) && - fn.localTypes.at(operation.use.ordinal)?._tag !== 'EffectBorrow' - ? [operation.use.ordinal] - : []), - ...(CleanupPlan.hasHook(operation.conflictCleanup) && - fn.localTypes.at(operation.onConflict.ordinal)?._tag !== 'EffectBorrow' - ? [operation.onConflict.ordinal] - : []), - ] - : []), - ...(operation._tag === 'Match' - ? operation.arms.flatMap((arm) => - arm.selected.cleanup.flatMap((entry) => - CleanupPlan.hasHook(entry.cleanup) && - fn.localTypes.at(entry.destination.ordinal)?._tag !== 'EffectBorrow' - ? [entry.destination.ordinal] - : [], - ), - ) - : []), - ] - }), + // A hook-bearing release passes `&mut self` into its hook, so its MIR owner needs frame storage. + ...MirVerification.operations(fn).flatMap((operation) => + operationCleanupEntries(operation).flatMap((entry) => + entry.local !== undefined && + CleanupPlan.hasHook(entry.cleanup) && + fn.localTypes.at(entry.local.ordinal)?._tag !== 'EffectBorrow' + ? [entry.local.ordinal] + : [], + ), + ), ]) const roots = new Map() let cursor = 0 @@ -352,7 +344,7 @@ export const framePlan = (fn: Mir.MirFunction, plan: LayoutPlan.Plan): FramePlan } } const nominalUnionCleanupScratch = nominalUnionScratchRequirements( - operations.flatMap(operationCleanupPlans), + [...operations.flatMap(operationCleanupPlans), ...additionalCleanups], plan, ).map((requirement) => { cursor = alignUp(cursor, requirement.alignment) diff --git a/packages/compiler/test/ExecutionPackage.test.ts b/packages/compiler/test/ExecutionPackage.test.ts index c1b0e574b..c3fba59f1 100644 --- a/packages/compiler/test/ExecutionPackage.test.ts +++ b/packages/compiler/test/ExecutionPackage.test.ts @@ -64,6 +64,60 @@ effect fn program() -> () ! Allocator.OutOfMemoryError { effect fn recover(error: Allocator.OutOfMemoryError) -> () { return () } pub fn main() -> () { return run Effect.catchAll(program(), recover) }` +const nominalUnionExecutionCleanup = `import silk.allocator { Allocator } +import silk.effect as Effect +import silk.execution as Execution +import silk.i8 as i8 +import silk.layout { Layout } + +struct Guard { + left: i8 + right: i8 + storage: Allocation +} + +impl Drop for Guard { + fn drop(self: &mut Guard) -> () { + let observed = i8.toI32(self.left) + i8.toI32(self.right) + if observed != 42 { + let boom = 1 / 0 + } + return () + } +} + +union Ready { + Small { marker: i8, guard: Guard }, + Wide { value: i64 } +} + +fn ready(state: &Ready) -> () { return () } + +effect fn packaged() -> () ! Allocator.OutOfMemoryError ? &mut Allocator { + let cleanupLayout = Layout.of() + let cleanupStorage = run Allocator.allocate(move cleanupLayout) + let state = Ready.Small { + marker: i8.toI8(7), + guard: Guard { + left: i8.toI8(19), + right: i8.toI8(23), + storage: move cleanupStorage + } + } + let execution = run Execution.make(effect { return 42 }, move state, ready) + drop execution + return () +} + +effect fn program() -> () ! Allocator.OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + return run packaged() |> Effect.provideMut(&mut allocator) +} + +effect fn recover(error: Allocator.OutOfMemoryError) -> () { return () } + +pub fn main() -> () { return run Effect.catchAll(program(), recover) }` + it('plans exact direct, nested, and externally parkable combined packages', () => { const target = Target.wasm32UnknownUnknown const layouts = Object.freeze({ @@ -268,6 +322,23 @@ it.effect('executes exact stored body and endpoint cleanup on WebAssembly packag }), ) +it.effect('reserves nominal-union scratch for synthetic execution cleanup helpers', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'execution-package/nominal-union-cleanup-frame', + new TextEncoder().encode(nominalUnionExecutionCleanup), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual(evaluated._tag, 'Completed') + + const artifact = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(artifact.bytes.slice()), {}) + ;(instance.exports.silk_main as () => void)() + }), +) + it.effect('drives one direct package to completion on an independent logical root', () => Effect.gen(function* () { const snapshot = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/IntegerScalars.test.ts b/packages/compiler/test/IntegerScalars.test.ts index 7d3ecaee7..0284ad588 100644 --- a/packages/compiler/test/IntegerScalars.test.ts +++ b/packages/compiler/test/IntegerScalars.test.ts @@ -203,6 +203,86 @@ it.effect('cleans the unused affine carrier and invokes the selected carrier exa }), ) +const checkedScalarNominalUnionCleanup = `import silk.i8 as i8 +import silk.u8 as u8 + +union Checked { + Present { value: T }, + Absent +} + +struct Guard { + left: i8 + right: i8 +} + +impl Drop for Guard { + fn drop(self: &mut Guard) -> () { + let observed = i8.toI32(self.left) + i8.toI32(self.right) + if observed != 42 { + let boom = 1 / 0 + } + return () + } +} + +union Choice { + Small { marker: i8, guard: Guard }, + Wide { value: i64 } +} + +fn present(value: u8, choice: Choice) -> Checked { + drop choice + return Checked.Present { value: value } +} + +fn absent() -> Checked { + return Checked.Absent +} + +fn presentWith(choice: Choice) -> some Checked> F { + return present(move choice) +} + +fn value(self: Checked) -> i32 { + return match move self { + Checked.Present { value } => u8.toI32(value) + Checked.Absent => 42 + } +} + +pub fn main() -> i32 { + let choice = Choice.Small { + marker: i8.toI8(7), + guard: Guard { left: i8.toI8(19), right: i8.toI8(23) } + } + let failed = Intrinsic.u8CheckedAdd>( + u8.toU8(255), + u8.toU8(1), + presentWith(move choice), + absent + ) + return value(move failed) +}` + +it.effect('reserves roots and nominal-union scratch for unused checked-scalar callbacks', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'integer/checked-scalar-nominal-union-cleanup', + new TextEncoder().encode(checkedScalarNominalUnionCleanup), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual(evaluated._tag, 'Completed') + if (evaluated._tag === 'Completed') assert.strictEqual(evaluated.result.value, 42n) + + const artifact = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(artifact.bytes.slice()), {}) + assert.strictEqual((instance.exports.silk_main as () => number)(), 42) + }), +) + const characters = `import silk.u32 as u32 import silk.char { fromU32, toU32 } import silk.option { Option } From b7db9a36f7afaa4e71219969741b8778733f65aa Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 28 Aug 2026 23:09:04 -0300 Subject: [PATCH 42/42] fix(wasm): close execution cleanup gaps --- .../specs/bootstrap-analysis-facade/spec.md | 1 - .../specs/bootstrap-hir/spec.md | 1 - .../specs/bootstrap-lexer/spec.md | 1 - .../specs/silk-source-formatting/spec.md | 1 - .../src/ToolchainIntegrity.generated.ts | 2 +- packages/compiler/src/WasmBackend.ts | 74 ++++++-- packages/compiler/src/WasmMemory.ts | 5 + .../compiler/test/ExecutionPackage.test.ts | 163 ++++++++++++++++++ .../compiler/test/WasmHeapReclaim.test.ts | 54 ++++++ 9 files changed, 287 insertions(+), 15 deletions(-) diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md index 18bb05878..99200c281 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-analysis-facade/spec.md @@ -17,4 +17,3 @@ syntax. - **WHEN** one variant is damaged beside valid siblings - **THEN** facade queries expose its unavailable state while retaining navigable facts for the valid variants and unrelated declarations - diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md index c2736cc57..72dc3c60b 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-hir/spec.md @@ -16,4 +16,3 @@ access mode, and active-variant cleanup without erasing a union to a structural - **WHEN** a match selects `HttpError.Dns` directly from `HttpError | OutOfMemoryError` - **THEN** HIR retains both the outer structural member and inner nominal variant selection with exact bindings and cleanup - diff --git a/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md b/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md index 25525d948..47f842cff 100644 --- a/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md +++ b/openspec/changes/add-nominal-unions/specs/bootstrap-lexer/spec.md @@ -9,4 +9,3 @@ identifier and SHALL retain exact source provenance under the existing trivia an - **WHEN** source contains `union` and `unionize` - **THEN** the first token is the union keyword and the second remains one identifier token - diff --git a/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md b/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md index f4b1da107..3054f4012 100644 --- a/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md +++ b/openspec/changes/add-nominal-unions/specs/silk-source-formatting/spec.md @@ -17,4 +17,3 @@ idempotent without changing variant or field identity. - **WHEN** construction or a pattern spells `Result.Success { value }` - **THEN** formatting preserves the applied parent before the dot and formats the field body under the ordinary struct-like policy - diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index 85e741d2d..3a1e03e50 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = 'b35797a2b76e1a18130a114b917538fc55a8bcff9cd66d1f771e5d29e6be4b57' +export const compilerDigest = 'a8606794b66ef844c3934b7fc9fbd734a9f0e8b77edd854cc58c199176f7c9dc' diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index cf2317d82..fde48a0ef 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -2546,6 +2546,57 @@ const makeOperationContext = ( ]), }) } + case 'LocalSharedCoreCleanup': { + const elementLayout = LayoutPlan.entry(plan, plan_.element) + const block = + elementLayout === undefined + ? undefined + : LocalSharedControlBlock.plan(plan.target, plan_.element, elementLayout) + if (block?._tag !== 'LocalSharedControlBlockPlan') { + throw new RangeError('Wasm address cleanup lost its local-shared control block') + } + const base = load() + const context = requireMemory() + const decrement = [ + ...base, + ...base, + Instr.memoryAccess('i32.load', context.memory, { offset: block.strongOffset }), + Instr.i32Const(1), + Instr.op('i32.sub'), + Instr.memoryAccess('i32.store', context.memory, { offset: block.strongOffset }), + ] + const last = [ + ...semanticLanesOf(plan_.element).flatMap((lane) => { + const offset = LayoutVerify.laneOffset(plan, plan_.element, lane.path) + if (offset === undefined) { + throw new RangeError('Wasm address cleanup lost a local-shared payload lane') + } + return [ + ...base, + Instr.memoryAccess(laneLoadMnemonic(plan, lane), context.memory, { + offset: block.valueOffset + offset, + }), + ] + }), + Instr.call(resolve(LocalSharedPayloadCleanup.declaration, [plan_.element])), + Instr.op('drop'), + ...base, + Instr.memoryAccess('i32.load', context.memory, { + offset: + block.allocationOffset + aggregateFieldOffset(SilkType.allocation, '$context'), + }), + Instr.call(requireRelease()), + ] + return Object.freeze({ + before: Object.freeze([ + ...base, + Instr.memoryAccess('i32.load', context.memory, { offset: block.strongOffset }), + Instr.i32Const(1), + Instr.op('i32.gt_u'), + Instr.ifElse(Instr.emptyBlockType, decrement, last), + ]), + }) + } case 'AllocationCleanup': case 'RawBufferCleanup': { const contextOffset = SilkType.isRawBuffer(plan_.type) @@ -8556,8 +8607,19 @@ const emitProgramUnmapped = Effect.fnUntraced(function* ( transferResultOffset + transferResultSize, program.layout.target.pointerAlignment, ) + const executionDriveCleanups = [...executionPackageCleanups.values()].flatMap( + (packageCleanup) => [packageCleanup.body, packageCleanup.endpoint, packageCleanup.callback], + ) const frames = new Map( - program.functions.map((fn) => [fn, framePlan(fn, program.layout)] as const), + program.functions.map((fn) => { + const drivesExecution = MirVerification.operations(fn).some( + (operation) => operation._tag === 'ExecutionDrive', + ) + return [ + fn, + framePlan(fn, program.layout, drivesExecution ? executionDriveCleanups : []), + ] as const + }), ) const staticOffsets = new Map() let staticEnd = 16 @@ -9018,15 +9080,7 @@ const emitProgramUnmapped = Effect.fnUntraced(function* ( }), ]), }) - const helperFrame = framePlan( - helperFunction, - program.layout, - [...executionPackageCleanups.values()].flatMap((packageCleanup) => [ - packageCleanup.body, - packageCleanup.endpoint, - packageCleanup.callback, - ]), - ) + const helperFrame = framePlan(helperFunction, program.layout, executionDriveCleanups) const helperLayout = layoutOf(helperFunction, program.layout, helperFrame, debug, false) const helperMemory: MemoryContext = Object.freeze({ memory: privateMemory, diff --git a/packages/compiler/src/WasmMemory.ts b/packages/compiler/src/WasmMemory.ts index 1b20af4b3..aff803e45 100644 --- a/packages/compiler/src/WasmMemory.ts +++ b/packages/compiler/src/WasmMemory.ts @@ -56,6 +56,11 @@ export const operationCleanupEntries = ( Object.freeze({ cleanup: operation.presentCleanup, local: operation.present }), Object.freeze({ cleanup: operation.absentCleanup, local: operation.absent }), ]) + case 'ExecutionDrive': + return Object.freeze([ + Object.freeze({ cleanup: operation.completionCleanup, local: operation.onComplete }), + Object.freeze({ cleanup: operation.suspensionCleanup, local: operation.onSuspend }), + ]) case 'SharedWithMut': return Object.freeze([ Object.freeze({ cleanup: operation.useCleanup, local: operation.use }), diff --git a/packages/compiler/test/ExecutionPackage.test.ts b/packages/compiler/test/ExecutionPackage.test.ts index c3fba59f1..a35037323 100644 --- a/packages/compiler/test/ExecutionPackage.test.ts +++ b/packages/compiler/test/ExecutionPackage.test.ts @@ -118,6 +118,137 @@ effect fn recover(error: Allocator.OutOfMemoryError) -> () { return () } pub fn main() -> () { return run Effect.catchAll(program(), recover) }` +const nominalUnionDriveCallbackCleanup = `import silk.allocator { Allocator } +import silk.effect as Effect +import silk.execution as Execution +import silk.i8 as i8 +import silk.layout { Layout } + +struct Guard { + left: i8 + right: i8 + storage: Allocation +} + +impl Drop for Guard { + fn drop(self: &mut Guard) -> () { + let observed = i8.toI32(self.left) + i8.toI32(self.right) + if observed != 42 { + let boom = 1 / 0 + } + return () + } +} + +union Choice { + Small { marker: i8, guard: Guard }, + Wide { value: i64 } +} + +fn ready(state: &()) -> () { return () } +fn complete(state: (), value: i32) -> () { return () } + +fn suspend(state: (), execution: Intrinsic.Execution, choice: Choice) -> () { + drop execution + drop choice + return () +} + +fn suspendWith(choice: Choice) -> some) -> ()> F { + return suspend(move choice) +} + +effect fn packaged() -> () ! Allocator.OutOfMemoryError ? &mut Allocator { + let layout = Layout.of() + let storage = run Allocator.allocate(move layout) + let choice = Choice.Small { + marker: i8.toI8(7), + guard: Guard { + left: i8.toI8(19), + right: i8.toI8(23), + storage: move storage + } + } + let execution = run Execution.make(effect { return 42 }, (), ready) + return run Execution.drive(move execution, (), complete, suspendWith(move choice)) +} + +effect fn program() -> () ! Allocator.OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + return run packaged() |> Effect.provideMut(&mut allocator) +} + +effect fn recover(error: Allocator.OutOfMemoryError) -> () { return () } + +pub fn main() -> () { return run Effect.catchAll(program(), recover) }` + +const nominalUnionSeparatedDriveCleanup = `import silk.allocator { Allocator } +import silk.effect as Effect +import silk.execution as Execution +import silk.i8 as i8 +import silk.layout { Layout } + +struct Guard { + left: i8 + right: i8 + storage: Allocation +} + +impl Drop for Guard { + fn drop(self: &mut Guard) -> () { + let observed = i8.toI32(self.left) + i8.toI32(self.right) + if observed != 42 { + let boom = 1 / 0 + } + return () + } +} + +union Ready { + Small { marker: i8, guard: Guard }, + Wide { value: i64 } +} + +fn ready(state: &Ready) -> () { return () } +fn complete(state: (), value: i32) -> () { return () } + +fn suspend(state: (), execution: Intrinsic.Execution) -> () { + drop execution + return () +} + +effect fn makeOne() -> Intrinsic.Execution ! Allocator.OutOfMemoryError ? &mut Allocator { + let layout = Layout.of() + let storage = run Allocator.allocate(move layout) + let state = Ready.Small { + marker: i8.toI8(7), + guard: Guard { + left: i8.toI8(19), + right: i8.toI8(23), + storage: move storage + } + } + return run Execution.make(effect { return 42 }, move state, ready) +} + +effect fn driveOne(execution: Intrinsic.Execution) -> () { + return run Execution.drive(move execution, (), complete, suspend) +} + +effect fn packaged() -> () ! Allocator.OutOfMemoryError ? &mut Allocator { + let execution = run makeOne() + return run driveOne(move execution) +} + +effect fn program() -> () ! Allocator.OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + return run packaged() |> Effect.provideMut(&mut allocator) +} + +effect fn recover(error: Allocator.OutOfMemoryError) -> () { return () } + +pub fn main() -> () { return run Effect.catchAll(program(), recover) }` + it('plans exact direct, nested, and externally parkable combined packages', () => { const target = Target.wasm32UnknownUnknown const layouts = Object.freeze({ @@ -339,6 +470,38 @@ it.effect('reserves nominal-union scratch for synthetic execution cleanup helper }), ) +it.effect('roots unused nominal-union callbacks released by execution drive', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'execution-package/nominal-union-drive-callback-cleanup', + new TextEncoder().encode(nominalUnionDriveCallbackCleanup), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + assert.strictEqual(Analysis.evaluate(snapshot)._tag, 'Completed') + + const artifact = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(artifact.bytes.slice()), {}) + ;(instance.exports.silk_main as () => void)() + }), +) + +it.effect('reserves package cleanup scratch when construction and drive are separated', () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'execution-package/nominal-union-separated-drive-cleanup', + new TextEncoder().encode(nominalUnionSeparatedDriveCleanup), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + assert.strictEqual(Analysis.evaluate(snapshot)._tag, 'Completed') + + const artifact = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(artifact.bytes.slice()), {}) + ;(instance.exports.silk_main as () => void)() + }), +) + it.effect('drives one direct package to completion on an independent logical root', () => Effect.gen(function* () { const snapshot = yield* Analysis.ofSourceRealized( diff --git a/packages/compiler/test/WasmHeapReclaim.test.ts b/packages/compiler/test/WasmHeapReclaim.test.ts index 62e7492ea..1539f71c7 100644 --- a/packages/compiler/test/WasmHeapReclaim.test.ts +++ b/packages/compiler/test/WasmHeapReclaim.test.ts @@ -238,6 +238,60 @@ it.effect( 120_000, ) +const nominalUnionSharedExecutionCleanup = `import silk.allocator { Allocator } +import silk.effect as Effect +import silk.execution as Execution +import silk.shared as Shared + +union Ready { + SharedState { value: Shared.Shared }, + Unit +} + +fn ready(state: &Ready) -> () { return () } + +effect fn packaged() -> () ! Allocator.OutOfMemoryError ? &mut Allocator { + let mut index = 0 + while index < 10000 { + let shared = run Shared.make(42) + let state = Ready.SharedState { value: move shared } + let execution = run Execution.make(effect { return 42 }, move state, ready) + drop execution + index = index + 1 + } + return () +} + +effect fn program() -> () ! Allocator.OutOfMemoryError { + let mut allocator = Allocator.systemAllocatorProvider() + return run packaged() |> Effect.provideMut(&mut allocator) +} + +effect fn recover(error: Allocator.OutOfMemoryError) -> () { return () } + +pub fn main() -> () { return run Effect.catchAll(program(), recover) }` + +it.effect( + 'reclaims local-shared payloads nested in nominal-union execution state', + () => + Effect.gen(function* () { + const snapshot = yield* Analysis.ofSourceRealized( + 'wasm-heap-reclaim/nominal-union-shared-execution-cleanup', + ascii(nominalUnionSharedExecutionCleanup), + 'wasm32-unknown-unknown', + ) + assert.deepEqual(Analysis.diagnostics(snapshot), []) + const evaluated = Analysis.evaluate(snapshot) + assert.strictEqual(evaluated._tag, 'Completed') + + const wasm = yield* Analysis.codegenWasm(snapshot, { mode: 'release' }) + const instance = new WebAssembly.Instance(new WebAssembly.Module(wasm.bytes.slice()), {}) + ;(instance.exports.silk_main as () => void)() + assert.isAtMost(pagesOf(instance), 4) + }), + 120_000, +) + const repeatedSharedAccess = `import silk.allocator { Allocator } import silk.allocator { Allocator, OutOfMemoryError, SystemAllocator } import silk.effect as Effect