Merge wasm → master: retarget Io at WebAssembly#495
Merged
stevedekorte merged 43 commits intomasterfrom Apr 20, 2026
Merged
Conversation
WASM.md: strategic direction, architectural decisions, phase 2 compacting collector design. WASM_PLAN.md: detailed phased plan from codebase audit — toolchain setup, io2c bootstrap, API stubs, platform cleanup, risk assessment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- cmake/wasi-sdk.cmake: toolchain file for wasi-sdk cross-compilation - All CMakeLists.txt: conditional static-only libs, skip -ldl/-msse2/ -lcurses/SANE_POPEN for WASI, IO2C_EXECUTABLE for host io2c bootstrap, hardcode USE_BUILTIN_NAN (can't run test programs when cross-compiling) - IoConfig.h: add WASM platform detection, clean up preprocessor structure - DynLib.c: WASM stubs (no dynamic loading) - IoSystem.c: WASM stubs for system(), platform(), activeCpus(), getpid() - IoFile.c: WASM stubs for popen/pclose - IoState_debug.c: skip signal() on WASI Native build and all tests still pass (25/25 C, 249/249 Io). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Guard setjmp.h includes for WASI (Base.h, IoNumber.c) - Guard signal.h in PortableStdint.h with sig_atomic_t fallback - Guard execinfo.h in IoCoroutine.c - Guard WIFEXITED/WEXITSTATUS macros in IoFile.c for WASI - Stub tmpfile() on WASM (not supported by WASI) - Fix CMake TODAY macro fallback for non-WIN32/non-UNIX platforms - Fix IO_SRCS paths in libs/CMakeLists.txt (iovm/io/ not io/) - Add missing Object_bootstrap.io to libs/CMakeLists.txt - Add WASI emulation libs (-lwasi-emulated-process-clocks, -lwasi-emulated-signal) - Fix Stack_asList function pointer type mismatch (WASM indirect call safety) - Fix IoInstallPrefix.h include ordering in IoSystem.c Io now compiles to WASM and runs correctly under wasmtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- IoFile remove: use rmdir() for directories on WASI (WASI's remove() only handles files, unlike POSIX which dispatches to rmdir/unlink) - IoSeq toBase: add errorRaised check after arg parsing and return after IoState_error_ (prevents divide-by-zero WASM trap) WASM test results: 225/225 compatible tests pass. Excluded: FileTest (popen), UnicodeTest (system()), SwitchTest (pre-existing). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all Windows/Symbian/multi-platform #ifdef code (~900 lines), extras/ directory (~3.9MB of dead IDE projects and syntax highlighters), and 7 CMakeLists.txt files. The codebase now targets WASM-only. - Remove WIN32, _MSC_VER, __SYMBIAN32__, __CYGWIN__, __MINGW32__ code paths - Remove extras/ (win32vc10, win32vc2005, symbian, osx, osxvm, xcode, IoLanguageKit, IoTest, SyntaxHighlighters) - Replace CMake build system with single 90-line Makefile - Simplify WASM guards to unconditional code (no setjmp, signal, execinfo) - Rewrite IoSystem.c, IoConfig.h, DynLib.c as WASM-only - Remove popen/subprocess tests (FileTest, UnicodeTest) that can't run on WASM - 25/25 C tests pass, 234/234 Io tests pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WASM has no dlopen — dynamic addon loading is dead code. Remove: - DynLib C implementation (basekit + iovm) - AddonLoader.io (addon discovery/loading) - DynLib.io (dynamic function calling wrapper) - FolderImporter and AddonImporter from Importer.io - DynLib proto registration from IoState.c Importer now only has FileImporter. 234/234 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eerie package manager depends on DynLib/addon loading which is not available on WASM. Remove the submodule entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hardcoded 28-line IO_FILES list in the Makefile with _imports.json, a recursive JSON manifest that declares .io file load order (following the strvct.net pattern). io2c now parses _imports.json via parson when argv[3] ends with .json, recursing into nested .json entries. Legacy CLI file list still works. Update parson submodule from 2018 (4f3eaa6) to 1.5.3 (ba29f4e). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser REPL (Phase 4a): - browser/io_browser.c: reactor WASM entry point with io_init/io_eval exports - browser/io.js: WASM loader + WASI shim + REPL UI logic - browser/index.html: dark-themed REPL matching iolanguage.org style - Makefile: `make browser` and `make serve` targets Docs: - docs2html.io: inline DocsExtractor instead of shelling out via System system() - gendocs.sh: use wasmtime + io_static instead of native io binary Plan: - WASM_PLAN.md: move compacting collector to Phase 5, expand Phase 4 with 4a/4b Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Large "io" header with "/ repl" subtitle matching original site - Dark #191919 theme, helvetica/Helvetica Neue Bold fonts - Full-height scrolling output area with left border accent - Topic buttons (HELLO WORLD, MATH, etc.) in bottom toolbar - Status text hidden once VM is ready - Added missing WASI shim functions (fd_fdstat_set_flags, path_*, fd_readdir, etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hand-wrapped DOM CFunctions with a generic bridge that lets Io code call any JS object and JS code send messages to any Io object. Value types (numbers, strings, bools, lists, maps) copy across the boundary via a binary serialization protocol in a shared 64KB buffer. Complex objects (DOM elements, functions) pass by reference via integer handles. See agents/IoJsBridge.md for full design documentation. - New: io_js_bridge.c/h — JSObject proto with forward dispatch, binary serialization, ioHandles table, WASM exports for JS→Io - New: js import namespace in io.js — js_call, js_get_prop, js_set_prop, js_call_func, js_typeof, Io Proxy factory - New: browser test suite (31 tests) with Playwright runner - New: docs for browser target, DOM interop, testing reference - Removed: io_dom.c/h and dom JS namespace (~550 lines) - Modified: IoCoroutine.c GC marking hook for ioHandles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- agents/WASM_BRIDGE.md: Bridge convention for Io/JS data passing — primitives copy, containers deep-copy, objects proxy, plus decisions on undefined, TypedArrays, iterators, functions, async, exceptions, events, BigInt/Symbol rejection, and UTF-8 string encoding - UArray default encoding changed from ASCII to UTF-8 to align with WASM bridge (ASCII is a valid subset, so existing code is unaffected) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion proxy Add JsTypes undefined singleton, cycle detection for container serialization, BigInt/Symbol rejection, JS Map→Io Map / Set→List symmetry, TypedArray↔Vector bridging, and exception proxy handles with error messages. 44 browser tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents library choice (libtommath), implementation plan, wire format, and design decisions for adding BigInt support to the browser bridge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Merge WASM.md + WASM_Plan.md → wasm/Plan.md - Merge WASM_Bridge.md + IoJsBridge.md → wasm/Bridge.md - Merge C_STACK_ELIMINATION_PLAN.md + CONTINUATIONS_TODO.md → stackless/Continuations.md - Keep BigInt.md, Other_Bridges.md, Eval_Loop_Design.md, Debug_Loop_Leak.md standalone Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New js_new_function WASM import wraps new Function(...) on JS side - New jsfunction CFunction on Lobby takes code string, returns JSObject - Syntax errors in code string propagate as Io exceptions - 6 new browser tests (50/50 passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JS Promises are now auto-detected and wrapped as IoFuture objects. Provides await, isReady, state, label methods. Forward delegates unrecognized messages (then, catch) to the underlying JS Promise. Adds io_resolve_future/io_reject_future WASM exports, js_watch_promise import, and 9 new browser tests (66/66 passing). Reorganizes plan docs into subplans/ directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Future await on a pending Promise now suspends the coroutine and yields to the JS host. When the Promise settles, JS calls io_resume_eval to restore the suspended coro and re-enter the eval loop. Key pieces: - FRAME_STATE_AWAIT_JS in eval loop state machine - awaitingJsPromise + suspendedCoro fields on IoState - io_resume_eval WASM export with chained await support - JS-side resumeIfAwaiting() + pendingAsyncResolve callback - Fixed coro save to use active coroutine (not self) so try() across suspend/resume works correctly with child coro frame swap 68/68 browser tests passing (2 new asyncAwait tests). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ync Step 5) When a message isn't found on a Future, the forward method handles it: resolved Futures redirect the eval frame to the resolved value (no nested eval); pending Futures yield to JS via AWAIT_JS (same as explicit await). Key fixes: LOOKUP_SLOT checks needsControlFlowHandling after IoObject_forward() calls; AWAIT_JS resume transitions to LOOKUP_SLOT (not ACTIVATE) so both explicit await and implicit forward resume correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New demo buttons: JS MATH (Math.sqrt), DOM (createElement + style),
ASYNC (Promise auto-await), CALLBACK (setTimeout with Io block),
COLORS (foreach + DOM), BUTTON (interactive click counter with
addEventListener). Fixed property setting to use set("textContent", ...)
instead of non-existent setTextContent method.
REPL now handles async eval (status=2) — shows spinner while awaiting,
updates in-place when Promise resolves. Background callback output
(from setTimeout/event handlers) is flushed to the REPL output area.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement arbitrary-precision integers using libtommath, with full round-trip serialization across the Io-JS bridge as TYPE_BIGINT (13). - Vendor libtommath v1.3.0 (public domain, pure C, WASM-compatible) - IoBigInt type: arithmetic (+,-,*,/,%,**), comparison, conversion - Wire format: decimal string [13][len:u32][string] — lossless any size - JS BigInt → Io BigInt and back without data loss - Add -D__STDC_IEC_559__ for mp_set_double on WASM targets - 11 new browser tests (83/83 pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CFunction activate path in FRAME_STATE_ACTIVATE used `goto continue_chain` to skip the loop-top refresh of frame/fd from state->currentFrame. When the activateFunc was IoObject_exit, it set shouldExit=1 and called IoCoroutine_rawResume to swap coroutines; the nested eval loop then popped all frames on its shouldExit check, leaving the outer loop's frame/fd pointers dangling into a cleared frame. The next CONTINUE_CHAIN iteration dereferenced a NULL fd->message and crashed. Check state->shouldExit after activateFunc returns and break to restart the loop so the top-level handler pops any remaining frames. Reproduced with `CLI version` (inlineMethod body ending in `System exit`). Fixes #493. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously `true asJson`, `false asJson`, `nil asJson` raised "does not respond to 'asJson'". Now they return the JSON literal strings "true", "false", "null". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously Sequence removeAt mutated and returned self, inconsistent with List removeAt which returns the removed item. Now Sequence removeAt (and therefore Vector removeAt) returns the element that was removed as a Number, matching the convention of Sequence at(i). Returns nil if the index is out of bounds, matching Sequence at's out-of-bounds handling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously 'list(Object clone) asString' produced multiline output with memory addresses (from Object asString). Mapping each element through asSimpleString first keeps the list representation on one line and matches the behavior already used by asSimpleString. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously 'Map with("k", "v") == Map with("k", "v")' returned false
because Map had no compare function and fell through to default
identity comparison. Now Maps with the same keys and equal values
compare as equal, matching the behavior of List.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous doc claimed moveTo raises a nameConflict exception if the target exists, but the implementation explicitly removes the target before renaming. Update the doc to describe the actual silent-overwrite behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tokensForString("()") (and "[]" and "{}") previously returned line 0
for the first token — the synthetic group-name identifier is inserted
before any character has been consumed, so currentLineNumber ran with
'current' sitting exactly on index[0] (the start-of-input sentinel) and
its <= check returned line 0. Lines are 1-based, so clamp the result.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The continuation loop was joining lines with bare string concatenation,
so tokens at the end of one line fused with tokens at the start of
the next. Insert an explicit '\n' between lines so the lexer sees a
separator.
Before:
Io> a := method(
... "hi" print
... nil
... )
==> method("hi" printnil)
After, the body parses as "hi" print ; nil as intended.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the coercion in IoState_runCLI that set exitResult to the value of the script's final expression when it was a Number. Exit codes now come only from explicit System exit(n) or from unhandled exceptions (-1), matching user expectations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hex/octal literal parsing used unsigned int (32-bit), causing values above 2^32 to be truncated. Number asString used int (32-bit) for the integer-form check, so large integers fell into scientific-notation formatting. Sequence toBase used uintptr_t, which is 32-bit on wasm32. Switch all three to unsigned long long / long long so integers up to 2^53 (double's exact-integer range) round-trip through literals, asString, and toBase without loss of precision. Update NumberTest testAsString: previously expected the buggy "2.147484e+09" for 2147483648 asString; now expects the correct decimal "2147483648". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Date_convertToTimeZone_ shifted tv_sec to make localtime() display the target zone's wall-clock time, which broke asNumber's contract. After convertToUTC the underlying timestamp no longer matched the unix epoch. Unix timestamps are timezone-invariant by definition — changing the timezone of a Date should not change the instant it points to. Drop the tv_sec shift so tv_sec always holds UTC seconds, and asNumber now returns the correct value regardless of conversion history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Object isKindOf used List contains, which compares with ==. For types that override == with structural equality (List, Map, Sequence), a fresh clone compared equal to its proto — so `List isKindOf(List clone)` returned true. Switch to containsIdenticalTo so ancestor membership is tested by object identity, which is what prototype lineage actually means. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
IoVMInit.c is generated by io2c from .io files in libs/iovm/io/ via make regenerate; matches *Io*Init.c* in .gitignore. My previous commit force-added it; this un-tracks it again. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous README documented a CMake/Eerie/MinGW/MSVC build matrix and addon install flow — none of which apply to the WASM build. Replace with a focused reference: wasi-sdk + wasmtime, the Makefile targets actually used, the WASI --dir= invocation pattern, and a pointer to the JS bridge for host integration. Keep the language intro, example code, and quick links; route users who need native builds and dynamic addons to the legacy native branch / tag. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tighten prose (drop filler phrasing, prefer active voice), restore standard markdown heading hierarchy, trim redundant REPL output in the Objects example, and add a dedicated Browser REPL section. Table of make targets now includes browser, serve, and check-browser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
README.md: correct the legacy-branch tag name to 2026.04.20-native-final (matches existing YYYY.MM.DD-suffix convention, e.g. 2026.03.06-stackless-alpha). ci.yml: replace the CMake-based Unix/Windows matrix with a single Ubuntu job that installs wasi-sdk + wasmtime and runs `make check` plus the browser build. Keeps the clang-format lint job unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Apr 20, 2026
Prepares wasm for fast-forward into master. Uses the 'ours' merge strategy because every change on master (bug-fix cherry-picks) is already represented on wasm with different SHAs — there is no unique content on master to preserve. After this commit, PR #495 (wasm → master) becomes fast-forwardable.
IoInstallPrefix.h was previously generated by CMake and gitignored, so fresh clones in CI couldn't build. The file is a single #define that has no WASM-specific meaning; commit it directly and drop the gitignore entry. Bump DoozyX/clang-format-lint-action v0.12 → v0.17 (v0.12 fails on current Ubuntu runners because Python 3.12 removed distutils). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
testAsNumber compared stringified NaN values, which varies across
libc / wasi-sdk versions ("nan" vs "NaN" vs "nan(0x...)"). Passes
locally (macOS) but fails under CI's wasi-sdk 24 on Ubuntu. Switch
to isNan checks, which are format-agnostic.
Drop the clang-format lint job: v0.12 is incompatible with modern
Python; v0.17 with clang-format 16 produces hundreds of diffs
against a codebase formatted for clang-format 12. Reformatting is
a separate cleanup, not a merge-day concern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This merges the
wasmbranch intomaster, retargeting Io at WebAssembly (WASI) as its primary platform. The previous native build (CMake + Eerie + DynLib) is preserved on thenativebranch and tagged2026.04.20-native-final.What changes for users
build/bin/io_static) runs under wasmtime, Node, and browsers via WASI shims — no per-platform builds.make servebrings up an Io REPL in the browser (browser/io_browser.wasm), with a bidirectional Io ↔ JavaScript bridge.awaitJS promises; JS can call Io methods that return futures. Unrecognized messages on a future implicitly auto-await.Makefiledriven bywasi-sdk; no CMake, no autoconf, no per-addon submodules.What goes away
The following are not available on WebAssembly and are gone from master:
DynLib,AddonLoader, and the entire dynamic-addon plugin systemextras/(SQLite, OpenGL, OpenSSL, Socket, etc. — anything that depended on native dynamic loading)If you depend on any of the above, use the
nativebranch. It accepts bug fixes; new development lands onmaster.How it was built
This lands on top of the stackless/iterative evaluator work (#488): Io's eval loop is now a heap-allocated frame state machine, which made first-class continuations, portable coroutines, and the JS async bridge possible without platform-specific assembly or
setjmp/ucontext.Merge strategy
Merge as a merge commit (not squash or rebase) to preserve the full history of the wasm branch.
Test plan
make checkpasses on wasm: C suite (25/25) + Io suite (234/234)make browserbuildsio_browser.wasm.github/workflows/ci.yml) green on the PR — the workflow has been rewritten to installwasi-sdk+wasmtimeand runmake check(the old CMake matrix is gone)🤖 Generated with Claude Code