Skip to content

Native Data.Int.Bits intrinsics; fix a dropped perform of user Effect functions - #42

Merged
katsujukou merged 11 commits into
purs-wasm:mainfrom
harryprayiv:bitwise-update
Jun 17, 2026
Merged

katsujukou merged 11 commits into
purs-wasm:mainfrom
harryprayiv:bitwise-update

Conversation

@harryprayiv

@harryprayiv harryprayiv commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Three changes, two independent:

  1. Feature — Data.Int.Bits now lowers to native i32 instructions instead of JS foreigns.
  2. Bugfix — a discarded perform of a non-inlined user Effect function no longer silently drops its effect.
  3. Test — an e2e regression guard for (2), in the routinely-run lane.

(1) and (2) are separable into two PRs if you'd rather keep them atomic; (3) belongs with (2).

Checklist

CI passing is enforced by the required ci-gate status check, not by a box here.
The items below are the human-judgment gates.

  • Docs / ADR — two notes still owed: the new Data.Int.Bits intrinsic family in the Intrinsics prose, and the knownFuncs exclusion in ADR-0015/0016 (foreignSigs covers all top-level values, so the host-foreign predicate must blacklist knownFuncs). Neither is written yet.
  • Tests — e2e guard for the dropped-perform fix (E2E.PerformUserEffect, routinely-run lane, not test:bin). The bit ops are exercised end-to-end by a downstream pure-PureScript SHA-3 implementation whose NIST vectors now pass on wasm; a dedicated compiler-side e2e for the bit ops is not yet added.
  • No perf regression — node bench/run.mjs across 3 runs. Heavy/stable benchmarks pinned at 1.00x; the two lightest (bintreeDfs, curry) swing ±~10% run-to-run and center on 1.0x (bintreeDfs measured both 1.11x and 0.86x), i.e. noise, not a regression. Baseline unchanged.
  • Runtime GC-type / ABI / canonicalization — N/A for both changes. No value-type substrate or host/runtime ABI change; bit ops are straight i32 instructions, and the perform fix saturates within the existing eqref / eval-apply convention.

harryprayiv and others added 7 commits June 16, 2026 01:58
…opping it

A discarded `perform` of a non-inlined user-defined `Effect`-returning function (e.g. `perform (bad "x")`) silently dropped its effect: `isEffectForeignApp` treated any binding with an `MEffect` *reconstructed* signature (ADR-0016) as a host foreign, so it was lowered as a bare producer value — a partial closure built but never applied to the perform-unit — instead of a direct call. The fix excludes `knownFuncs` (anything with a decl body) from the host-foreign check, mirroring the existing intrinsic / `foreignIntrinsic` exclusions, and lowers a performed non-foreign application by appending the perform-unit to the producer's own argument list (ADR-0018) so it saturates to a direct `RCallKnown`. Host foreigns (`log`) are unaffected — the host still performs the returned thunk. Refs ADR-0015 (perform lowering), ADR-0016 (signature reconstruction), ADR-0018 (arity includes the perform-unit).
… Effect fns This bug was invisible: nothing asserted that a discarded `perform (f x)` actually ran, so the dropped effect failed silently with a fully green suite. This adds the guard that would have caught it on day one. Adds fixture E2E.PerformUserEffect + its Cli spec. `bump` is a user-defined Effect function padded past the inline cap and referenced more than once, so its call site survives as `perform (bump k)` — the exact path that regressed; a small/inlined helper would pass and prove nothing. `bump` performs a host foreign (`record`), so it lands in impureKeys and its perform survives the simplifier — pinning any failure to lowering, not purity. The effect is read back as an accumulated total via callI32x1; that total equals the sum of the bumped values iff each perform ran exactly once (catching both drops, which read low, and accidental duplications, which read high). Three cases cover the discard paths that all route through the same isEffectForeignApp fix: do-notation (bindBody) => 11, void (mapBody) => 2, and `*>` (applyBody) => 7. Lives in the routinely-run e2e lane, not test:bin.
@harryprayiv

harryprayiv commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor Author

I ran the benchmarks just now too:

wasm: 9007 bytes  (Bench.Main bundle)

fib         20:28.1us  22:73.7us  24:191.6us  26:503.1us  28:1.3ms
sumLoop     200000:106.7us  400000:213.9us  600000:320.5us  800000:426.8us  1000000:533.7us
qsort       500:93.8us  1000:242.1us  1500:407.8us  2000:530.7us  3000:926.6us
nqueens     6:9.1us  7:40.7us  8:254.3us  9:1.5ms
bintreeDfs  12:41.4us  13:79.7us  14:152.2us  15:340.7us  16:777.0us  17:1.8ms
bintreeBfs  8:340.0us  9:1.5ms  10:4.3ms  11:15.4ms  12:70.9ms
mapFold     100:388.3us  200:705.7us  300:1.1ms  400:1.4ms  500:1.7ms
mapFoldArray 100:269.6us  200:518.4us  300:768.2us  400:1.0ms  500:1.3ms
countEffect 1000:492ns  2000:969ns  4000:1.9us  8000:3.8us  16000:7.6us  32000:15.3us  64000:30.5us
curry       50000:3.0ms  100000:6.1ms  200000:10.8ms  400000:24.3ms  800000:48.6ms

wrote .../purescript-backend-wasm/bench/snapshots/20260616-215016/results.json + 10 *.dat files

vs baseline (largest input):
  fib         1.3ms -> 1.3ms  (0.99x)
  sumLoop     0.5ms -> 0.5ms  (1.00x)
  qsort       0.9ms -> 0.9ms  (1.00x)
  nqueens     1.5ms -> 1.5ms  (1.00x)
  bintreeDfs  1.6ms -> 1.8ms  (0.86x)
  bintreeBfs  71.2ms -> 70.9ms  (1.00x)
  mapFold     1.7ms -> 1.7ms  (1.00x)
  mapFoldArray 1.3ms -> 1.3ms  (1.00x)
  countEffect 0.0ms -> 0.0ms  (1.00x)
  curry       44.1ms -> 48.6ms  (0.91x)
snapshot -> bench/snapshots/20260616-215016

@katsujukou
katsujukou self-requested a review June 17, 2026 14:19
@katsujukou katsujukou added bug Something isn't working feature labels Jun 17, 2026
@katsujukou

Copy link
Copy Markdown
Collaborator

Hi, thank you for this! Both changes are well-reasoned, and the commit messages explain the intent clearly.
However, we need you to fix a few things before this can be merged:

  1. Drop the environment / lockfile changes — they shouldn't be in this PR.
    flake.nix / flake.lock remove the maintainer's nix-claude-code dev-shell input and bump the nixpkgs / overlay pins; package-lock.json is a new npm lockfile (this repo is pnpm — pnpm-lock.yaml / pnpm-workspace.yaml), most likely from running npm install instead of pnpm install. Please restore them to main: git checkout main -- flake.nix flake.lock pnpm-lock.yaml spago.lock and remove the npm lockfile: git rm package-lock.json, so the PR is code-only.

  2. Delete headArity rather than commenting it out, if it's genuinely orphaned now.

  3. Restore the trailing newlines on Binaryen.purs, Intrinsics.purs, and Lower/Reps.purs (the \ No newline at end of file markers) — pnpm -F compiler run check / purs-tidy will flag these.

  4. The two owed doc notes you flagged in the checklist — with one correction to where the second belongs:

    • the new Data.Int.Bits intrinsic family in the Intrinsics prose; and
    • the knownFuncs exclusion in isEffectForeignApp. One precision: the misclassification does not originate in the ADR-0016 source reconstruction (parseForeignSigs only reconstructs foreign import declarations, so a non-foreign user function like bump is never in srcSigs).
      It originates in the externs-derived foreignSigs (Externs.foreignSigs), which is keyed off every EDValue — every exported top-level value, ordinary functions included, not just foreigns (the externs format doesn't mark foreign-ness; its own doc comment notes the extra entries are "inert" because foreign resolution only consults a sig where the name is an actual unresolved import).
      The bug is that isEffectForeignApp used the sig's MEffect result as a classifier for "is this a host foreign?", which breaks that "inert" assumption for an exported Effect-returning user function. So the knownFuncs exclusion is the right fix (anything with a decl body is by definition not an opaque host foreign — it mirrors the existing intrinsic / foreignIntrinsic exclusions in the same predicate).
      Please place the addendum on ADR-0015 (the perform / host-foreign lowering) noting Externs.foreignSigs's all-EDValue coverage, rather than on ADR-0016 — and please fix the commit-message attribution ("MEffect reconstructed signature (ADR-0016)") accordingly, since the reconstructed (source) path is not where this comes from.

Additionally, CI hasn't run on the branch yet (no ci-gate result) — please make sure npm run test (incl. test:e2e) is green locally with the lockfiles reverted; I'll get the workflow triggered on the PR.

By the way, the E2E.PerformUserEffect guard is nicely built (padding bump past the inline cap, plus the do / void / *> coverage) — exactly the right shape. Happy to merge once the above is addressed.

@harryprayiv

Copy link
Copy Markdown
Contributor Author

It's going to be a long time before I get around to all of that.

So, you have my full permission to either:

A: take what you like and delete the rest

B: wait a few weeks + for me to slowly make those cosmetic changes 💀

Thanks again for making this module.

@katsujukou

Copy link
Copy Markdown
Collaborator

OK, then can I push a few follow-up changes, including some documentation addenda and removing unnecessary project configuration changes (namely Nix and npm)? After that, I'll merge this PR!

@harryprayiv

Copy link
Copy Markdown
Contributor Author

Thanks! I'm still not great at formal PR's and I genuinely just want to help. :). If there's anything you need help on, I'll be here, using it in branches of my Prescript projects to try and uncover niceties/necessities that I could add and you can rest assured that my next PR will more closely follow your guidelines. 😊

@katsujukou
katsujukou merged commit e38475f into purs-wasm:main Jun 17, 2026
18 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants