Skip to content

Effect, Effect.Console and Test.Assert surfaces over the WASI library #95

Closed

Description

@biuld

Part of #76 (Phase 3). Remeasured 2026-10-03 on master at fe40d17: 258 passing files stop at Effect.Console, 43 at Effect, and 4 at Test.Assert — 305 files, by first blocking stage. No passing file blocks on Prelude, because stdlib/lib/Prelude.purs exists and resolves; its missing surface is latent rather than a first blocker, which is why #94 measures 0 by this convention.

We already have the runtime: WASI.Console wraps wasi:cli/stdout/stderr, and WASI-02/03 are Verified with execution tests. What is missing is the module surface the corpus expects.

Do

  • Effect and Effect.Console as a source-level surface over the existing WASI console, exporting log, logShow, warn, error and the Effect type the corpus imports.
  • Keep WASI.Console as the platform layer and have Effect.Console be a thin binding over it, so the WASI capability gates stay in one place.
  • Test.Assert over the same execution model: assert, assertEqual, assertThrows, assertFailure. A failing assertion must be observable as a non-zero exit, so passing assertion files score cleanly.

Note on DEC-11

docs/decision/DEC-11-primitive-ffi-stdlib-wrappers.md fixes the wrapper-versus-raw-import split. Effect.Console is a wrapper; the raw import stays inside WASI.Console. Do not collapse them.

Acceptance

Effect, Effect.Console, and Test.Assert exist, execute under Wasmtime, and the 305 blocked files drop toward zero.

Activity

  1. added
    gate:L6/M7Blocks runtime and the standard library (D-04 L6, M7)
    area:stdlibThe PureScript standard library under stdlib/lib
    on Oct 1, 2026
  2. biuld commented on Oct 3, 2026

    @biuld
    OwnerAuthor

    Second slice landed in #127: the unit value and Test.Assert.

    What landed

    • unit as a compiler primitive (Intrinsic::Unit). It lowers to Core ExprKind::Unit and to the canonical integer 0 only in CC, so no earlier stage reads it as an Int. unit and () reach the same value.
    • Test.Assert with assert, assert', assertTrue, assertFalse.
    • Prelude.trap, a psrs:effect external whose body is an unreachable path. A failed assertion writes its message to stderr and then traps; a guest trap, not a non-zero exit code, is the only failure signal a compilable corpus case can carry.

    PSRS_REQUIRE_WASMTIME=1 cargo test -p psrs-driver --lib tests::assertions executes both paths (held assertion exits 0; failed one writes first, then traps, and nothing after the trap runs).

    Measured (PSRS_ORACLE=annotations cargo test -p psrs-driver --test suite -- --ignored --nocapture)

    Board Before this slice After
    passing resolution 199/413 209/413
    missing library module 83 62
    P3 resolve 127 138
    L1 / L3 / L4 / L5 / L6 904/908, 34/48, 34/50, 51/92, 0/413 unchanged

    The 21 Test.Assert cases split into 10 that now resolve and 11 that stop on the next library gap; the unit half clears 11 unknown name unit blockers. The board's Corpus cases for this issue is now 18.

    What remains on this issue

    logShow (16 cases) and assertEqual (2 cases), both waiting on the Show/Eq class surface that #94 owns. assertThrows needs to observe a trap, which the target profile does not provide, so it is recorded as an obligation rather than approximated (D-04 M7, EF-12). The issue stays open until #94 lands that surface.

  3. biuld commented on Oct 4, 2026

    @biuld
    OwnerAuthor

    Effect.Console.logShow landed — #138

    logShow :: forall a. Show a => a -> Effect Unit is now defined as
    log (show value) over the Show class from Data.Show. It adds no I/O and no
    second stringifier: the rendering is the library's Show and the destination is
    still the one WASI.Console.log decides. Verified under Wasmtime by
    tests::assertions::log_show_writes_the_library_rendering, which asserts the
    renderings for Int, a negative Int, String, Char, Boolean, and
    Array Int.

    Evidence

    PSRS_REQUIRE_WASMTIME=1 PSRS_ORACLE=annotations cargo test -p psrs-driver --test suite -- --ignored --nocapture
    

    at 071fb11 plus this slice, one run of all five boards:

    Board Before After
    L1 parse 904/908 904/908
    L2 failing 72/72 72/72
    L3 35/48 35/48
    L4 35/50 35/50
    L5 53/80 53/80
    passing resolved 253/413 270/413
    M7 first blockers missing 53, P10 177, P3 103, P5t 42, P5k 17, P8 13, P6 4, P0 4 missing 53, P10 187, P3 86, P5t 46, P5k 17, P8 14, P6 6, P0 4
    L6/M7 runtime 0/413 0/413

    logShow was the largest single P3 name blocker at 24 cases, 19 of them reported
    as "logShow is not exported by the imported module". 17 resolve past L2; the
    other 7 stop at the next P3 name, which nothing shadowed before — Monad and
    compare 2 each, and negate, not, and _ 1 each. The 17 land at later stages
    as 10 P10, 4 P5 typecheck, 1 P8, and 2 P6.

    Board Corpus cases for this issue is now 2.

    assertEqual/assertEqual' are not landed, and the reason changed

    Both classes are declared, so this is no longer a library-surface gap — it is a
    compiler defect, filed as #137.

    The corpus calls the record form (passing/DerivingFoldable.purs and one other
    case):

    assertEqual' :: forall a. Eq a => Show a => String -> { actual :: a, expected :: a } -> Effect Unit

    A constraint whose quantified variable appears inside a record type is
    elaborated with the record as the constraint's argument, so the wanted
    constraint comes out as Eq { actual :: a, expected :: a } and the declaration is
    rejected with NoInstanceFound. Probes that separate this from record syntax:

    Signature Result
    forall a. Eq a => a -> Int accepted
    forall a. String -> { actual :: a, expected :: a } -> Int (no constraint) accepted
    forall a. { actual :: a, expected :: a } -> a — field access accepted
    forall a. { actual :: a, expected :: a } -> a — record pattern accepted
    forall a. Eq a => { actual :: a, expected :: a } -> Int rejected
    the same behind a type synonym for the record rejected

    A record whose fields are quantified variables is fine on its own, and both
    destructuring forms work on it; only the constraint combined with the record
    fails, and a type synonym does not change it, so this is constraint elaboration
    rather than record syntax or the parser. Changing the argument shape would change
    the API the corpus calls, so stdlib/lib/Test/Assert.purs records the blocker
    instead of approximating it.

    Still open on this issue

    assertEqual and assertEqual' (2 passing cases), blocked on #137.
    assertThrows/assertThrows' remain a separate gap: they need to observe that an
    argument trapped, which needs the Wasm exceptions proposal, outside DEC-05's
    profile.

    Status stays In Progress.

  4. biuld commented on Oct 16, 2026

    @biuld
    OwnerAuthor

    The 2026-10-17 runtime scoreboard no longer has a passing file whose first blocker is a missing Effect or Effect.Console surface. Corpus cases moves from 2 to 1.

    The remaining file is passing/TCOMutRec.purs, which stops at P8 library linking because Test.Assert.checkThrows has no target implementation. A trap aborts the instance, so that foreign cannot return Boolean. assert, assertEqual, and the console logs do execute. This issue stays open for checkThrows.

    Evidence: L6/M7 is 273/413 in docs/design/D-04-suite-roadmap.md, recorded by #145.

  5. biuld commented on Oct 19, 2026

    @biuld
    OwnerAuthor

    The library surfaces this issue owns exist and execute under Wasmtime, measured on master 93a5d31c (PRs #149–#155, the exception-channel adoption):

    • Effect and Effect.Console execute over the WASI console (long since landed); Test.Assert's assert/assertEqual/assertThrows all execute: checkThrows is implemented end to end over the adopted WebAssembly exception channel — the checked psrs:runtime-exception#checkThrows contract published (canonical CC signature, no host ABI), discharged at P9 into the inline catch_all try region, and verified through the real frontend: a guard around a genuinely non-tail rec at depth 100000 exhausts the measured stack budget (STACK_BUDGET=262144, weights 32+16×locals, pinned -W max-wasm-stack=524288, wasmtime 48.0.1) inside the guarded call and the catch delivers true, exit 0 (PSRS_REQUIRE_WASMTIME=1 driver evidence; stdlib 728a82f/fnv1a64-v1:5dfa6f2ce6b3092c, adaptation record docs/assert-throws.md).
    • The stable default profile enables the exception channel (DEC-23, amended 2026-10-19); L6/M7 re-measured 287/340 on 2026-10-19 (serial per-root, 10-second guest wall, release suite SHA-256 5d3bf058…): the failure-name set is identical to the 2026-10-18 baseline, and the only diff is TCOMutRec.purs's re-attribution from the P8 missing-foreign row to a recorded runtime-trap row — its four assertThrows assertions encode upstream's tail-call classification (the corpus's own comment says so) and are upstream-implementation-specific under the backend's mandated closure-tail reuse (DEC-23's amendment + the tail-call topic's boundary note). So Corpus cases = 0: no in-scope program is blocked on a missing Effect/Effect.Console/Test.Assert surface; the trap row is owned by the tail-call boundary, not a missing library module.
    • Remaining unimplemented JavaScript foreigns: Console timers, Effect.Exception, Effect.Aff (D-04's Effects row).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:stdlibThe PureScript standard library under stdlib/libenhancementNew feature or requestgate:L6/M7Blocks runtime and the standard library (D-04 L6, M7)

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions