Skip to content

FEA: Add a WebAssembly binding that computes the native build's bits - #27

Open
thclark wants to merge 4 commits into
mainfrom
wasm
Open

thclark wants to merge 4 commits into
mainfrom
wasm

Conversation

@thclark

@thclark thclark commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Promotes the wasm trunk to main. It carries one squash-merged PR, #21, plus the review changes to its workflows: a WebAssembly binding to yFoil whose output matches the native build bit for bit.

What it adds

  • bindings/wasm/ (yfoil-wasm, wasm-bindgen): naca, analyse and polar take and return the CLI's JSON (options keyed by its long flags, with its defaults), so a record computed in the browser is the same file yfoil writes for the same inputs. evaluate probes the maths library. The main crate is unchanged and compiles for wasm32-unknown-unknown as it is.
  • cr-libm/wasm-include/ and a wasm32-only include path in cr-libm/build.rs: the six C library headers CORE-MATH includes, stating WebAssembly's single rounding mode and its lack of exception flags. The sources and flags are the native ones. Native targets and the XFOIL reference (built by scripts/xfoil-build.sh, not by cr-libm) are untouched.
  • scripts/wasm.sh build | check | package, the tests bindings/wasm/tests/{identity,smoke}.mjs, .github/workflows/wasm.yml (jobs wasm, smoke) and .github/workflows/release.yml (jobs version, wasm, add-artifact).
  • Docs in docs/bindings.md.

Bit-identity evidence

scripts/wasm.sh check compares bytes against the release yfoil CLI. serde_json writes each f64 as its shortest round-trip text, so equal text means equal bits. 227 of 227 comparisons are identical:

  • 8 generated geometry sections
  • 5 analyses on each of the 41 tracked panel sets, including a −10° → 20° polar with --distributions --allow-invalid: 1129 solved points with every BL array
  • every cr_libm function and fma on 262 144 arguments each

NaNs are compared as a class: any NaN equals any NaN, while a NaN against a number still fails, and signed zeros and infinities are compared bit for bit. The reason: IEEE 754 and the WebAssembly spec leave a NaN's sign and payload unspecified, x86-64 and ARM natives already differ in them, and yFoil only ever tests is_nan and writes NaN as null. The NaN bit differences are counted as information (97 990 on x86-64, none on ubuntu-24.04-arm).

Packaging and release

.github/workflows/wasm.yml (jobs wasm and smoke) runs on PRs into and pushes to main, and can also be called as a reusable workflow:

  • wasm runs the full check, then scripts/wasm.sh package. This writes reproducible yfoil-wasm-<version>-{web,nodejs}.tar.gz (with yFoil's and CORE-MATH's licences) and SHA256SUMS, and uploads them as the artifact yfoil-wasm.
  • smoke downloads that artifact, verifies the checksums, loads both packages the way their users would, and requires the native record of the CI reference case.

.github/workflows/release.yml makes releases, artifact first:

  • It runs on pushes to main. Its version job stops if release v<crate version> already exists.
  • Otherwise it calls wasm.yml with that tag, so the assets are built, checked and smoke-tested from that commit.
  • Only then does add-artifact (contents: write, this job only) create the release with the assets attached, in one gh release create. The release title is the squash-merge commit's subject.
  • workflow_dispatch defaults to a dry run.

Merging this PR will create release v0.1.0: main is at 0.1.0 and has no release.

Untested until it runs on main: the gh release create call itself, and workflow_dispatch, which GitHub only offers for workflows on the default branch. Both workflows pass actionlint. The packaging, the checksum verification and the refusal of a tag whose version differs from the crates' were tested locally.

Versioning

No version bump. There is no pyproject.toml or semantic check in this repo, no release tag yet, and the crates stay at 0.1.0 as on main.

Comment thread .github/workflows/wasm.yml Outdated
Comment on lines +14 to +15
pull_request:
branches: [ "main", "wasm" ]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn;'t make any sense, why is it running against the wasm branch?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. The wasm filter was only there so the workflow ran on PRs into the wasm trunk while it was being built, and nothing needs it once this goes to main. The workflow now triggers on PRs into and pushes to main only, as build-and-test and reference-build do: 559ed27.

Comment thread .github/workflows/wasm.yml Outdated
path: expected
- run: node bindings/wasm/tests/smoke.mjs dist --expect expected/reference.json

release:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be called 'add-artifact'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed: the job that attaches the assets is now add-artifact. I read this together with your next comment, so it moved into the new release.yml rather than staying in wasm.yml: e79dde1.

Comment thread .github/workflows/wasm.yml Outdated
if [ "$DRY_RUN" = "true" ]; then
echo "dry run: would attach to $TAG:" && ls -l dist
elif gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" dist/yfoil-wasm-*.tar.gz dist/SHA256SUMS --clobber

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why isn't this being done as part of the release workflow? this allows a release to be made before the artifact is even built, makes no sense as a flow at all

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the flow was backwards. Fixed in e79dde1 by inverting it.

  • wasm.yml no longer reacts to published releases or tags. It is now also a reusable workflow (workflow_call with a tag input).
  • The new release.yml runs on pushes to main. Its version job reads the crate version and stops if release v<version> already exists.
  • Otherwise it calls wasm.yml with that tag, so the assets are built, checked bit for bit and smoke-tested from that commit.
  • Only then does add-artifact (contents: write, that job only) create the release with the assets attached, in one gh release create. A release can no longer exist before, or without, its artifacts.
  • The release title is the squash-merge commit's subject, which is the PR title, as docs/conventions/git-pull-requests.md describes for release.yml. workflow_dispatch defaults to a dry run.

My reading of "the release workflow": the repo has no release.yml yet, but the conventions refer to one, so I created it. One consequence to be aware of: main is at 0.1.0 and has no release, so merging #27 will create release v0.1.0 with the wasm assets. If you'd rather the first release waited, say so and I'll put the push trigger behind a condition.

…21)

* FEA: Add a WebAssembly binding that computes the native build's bits

- bindings/wasm (yfoil-wasm, wasm-bindgen 0.2.129): naca, analyse and polar take and return the
  CLI's JSON (options keyed by its long flags, with its defaults), so a record computed in the
  browser is the file `yfoil` writes for the same inputs; evaluate probes the maths library
- cr-libm/build.rs adds, for wasm32 only, the include path of cr-libm/wasm-include/: the six C
  library headers CORE-MATH includes, stating WebAssembly's single rounding mode and absent
  exception flags (wasi-libc's fenv.h lacks the macros the sources name). The sources and flags
  are the native ones; fma, round and the 128-bit helpers come from Rust's compiler-builtins
- scripts/wasm.sh build | check: builds the module (web and nodejs glue, wasm-opt when present)
  and compares it byte for byte with the native CLI: eight generated sections, five analyses of
  each of the 41 tracked panel sets (1129 solved polar points with every BL array) and every
  cr_libm function and fma on 262 144 arguments, all identical on aarch64 macOS
- the main crate is unchanged: it compiles for wasm32 as it is

* ENH: Compare NaNs as a class in the wasm check, and run it on x86-64

IEEE 754 and the WebAssembly spec leave a NaN's sign and payload unspecified, x86-64 and ARM
natives already differ in them, and yFoil only tests is_nan and writes NaN as null. The maths
probe now treats any NaN as equal to any NaN (a NaN against a number still fails; signed zeros
and infinities stay bit for bit) and counts the NaN bit differences as information. The
workflow moves to ubuntu-latest.

* OPS: Package the checked wasm module as a CI artifact and attach it to releases

- scripts/wasm.sh package [--tag TAG]: reproducible yfoil-wasm-<version>-{web,nodejs}.tar.gz
  (with yFoil's and CORE-MATH's licences) and SHA256SUMS from the packages check built; a tag
  whose version is not the crates' fails, and an untagged build is <version>-dev.<commit>
- bindings/wasm/tests/smoke.mjs: verifies the checksums, unpacks both archives, loads each as
  its users do and requires the native record of the CI reference case
- the WebAssembly workflow runs wasm-opt (binaryen 133, pinned by checksum), uploads the
  packages as the artifact yfoil-wasm, smoke-tests the artifact as downloaded, and on a
  published release or a v* tag attaches that run's assets with gh release upload (contents:
  write on that job only; a workflow_dispatch dry run skips the upload)
The wasm trunk filter only served the PR into the trunk; the workflow now triggers on PRs into
and pushes to main, like build-and-test and reference-build.
…act is checked

The WebAssembly workflow no longer reacts to published releases or tags, which let a release
exist before (or without) its assets. release.yml runs on pushes to main when the crate version
has no release yet: it calls wasm.yml (now also a reusable workflow) with the tag v<version>,
so the assets are built, checked and smoke-tested from that commit, and only then its
add-artifact job creates the release with them attached (gh release create, contents: write on
that job only). workflow_dispatch defaults to a dry run.
Main's unified validity CLI (#34) solves every point with its five 1-ULP twins by default and
passes the output options as one OutputOptions. The binding takes the same options with the same
defaults (conditioning, spread_warning_threshold, spread_error_threshold, and halt_on for polar),
so its records stay the CLI's byte for byte.

The CLI's halt-on and threshold parsing move into the library (HaltRule::parse_list,
Thresholds::from_options) so both use one copy, with the same messages. WebAssembly has no
threads, so on wasm targets (the module and the wasm32-wasip1 CLI) a point's twins run one after
another; they start from copies of the session and never steer the base, so the results are
the same. The bindings page's speed figures are remeasured with and without the twins.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant