The lws output wedge addressed in the previous commit was incompletely
understood: under genuine backpressure (peer slow or paused, kernel
send buffer full) a connection still froze permanently. Root cause,
established by tracing the writeable-request plumbing end to end:
lws_send_pipe_choked() is true not only when lws holds a truncated
send (lws re-arms the writeable callback itself then) but also when a
zero-timeout poll(POLLOUT) reports the socket simply full -- and in
that case every lws_write() has fully succeeded, lws has nothing
pending, and nobody re-arms anything. Fix, per lws README.coding.md:
whenever the drain loop exits with data still queued in pss->buffer,
request the next writeable callback. Queued data always has a callback
requested, so no exit path can strand output.
Also:
- LWS_CALLBACK_CLOSED frees the session evbuffer unconditionally: on
driver-initiated closes (e.g. the mudlib destructing the interactive)
close_user_websocket() nulls pss->user first, and the old early
return leaked the buffer every time.
- src/www/README.md + src/www/AGENTS.md: architecture doc and agent
checklist for the web terminal pages (xterm.js/telnet.js layering,
vendor policy, packaging, testing, the wedge mechanism).
- tools/ws-smoke.js, wired into CI on the Clang Debug matrix entries
(with and without sanitizers): a dependency-free node websocket
client that boots the real driver and exercises the http mount,
telnet + ascii subprotocols through the shared src/www/telnet.js,
SGA char-mode switching, live TUI streaming, TLS, and -- the actual
regression gate -- forced-backpressure bursts (paused socket, ~4.8MB)
on the plain and TLS ports plus a destruct-while-choked teardown
check. All three backpressure checks fail on the unfixed driver;
neither GTest nor the LPC suite exercises any websocket client
traffic.
- Fix stale src/www/wasm/vendor/ path references left from the vendor
directory move (src/wasm/README.md, docs/build-wasm.md, release.yml).
Validated on the ASan Debug build: forced-backpressure repros recover
the full burst on both subprotocols, destruct-while-choked clean under
ASan, ws-smoke 17/17, GTest 312/312, LPC testsuite clean.
One coherent change: make the mudlib TUI library (/std/tui) work in the
browser on BOTH web terminals, harden everything the work surfaced, and
pin the toolchain that broke it.
## Web terminals: xterm.js + a shared telnet client
* Vendor @xterm/xterm 6.0.0 + @xterm/addon-fit 0.11.0 (dist files
byte-exact from the official npm tarballs, licenses included) under
src/www/vendor/. xterm.js does the terminal emulation on both pages:
rendering, SGR 16/256/truecolor, alternate screen, cursor state, wide
characters, scrollback, mouse reporting, bracketed paste, and keyboard
encoding (the whole dialect testsuite/std/tui/keys.lpc decodes,
including C-_ undo).
* src/www/telnet.js -- one telnet option engine shared by both pages
(transport-agnostic; hooks for page-specific options): ECHO masks the
password prompt, WILL/WONT SGA -- the driver's char-mode signal
(set_charmode) -- automatically switches between the line-input bar
and raw keystroke streaming, NAWS reports real terminal geometry from
the fit addon and re-reports on resize (driving the window_size
apply), TTYPE answers xterm-256color.
* src/www/wasm/index.html (the wasm shell): output/input rides xterm.js;
the page keeps the synchronous-bridge queueing (sends flush outside
receive() -- the wasm bridge re-enters the parser otherwise), the
error modal, and the jsbridge handlers. Also guards
crypto.getRandomValues() against views backed by resizable
ArrayBuffers (see the emsdk section below).
* src/www/index.html (the websocket client for the native driver):
rewritten on the same stack, replacing a parseANSI() that stripped all
cursor sequences (no TUI possible) and a telnet layer whose option
bytes leaked into the text stream and never answered negotiations.
Passwords now mask, char mode works, GMCP/MSP kept (dead
TelnetOverWebSocket/handler classes removed); ws frames arrive as
arraybuffers (no Blob/FileReader path); UTF-8 and telnet sequences
survive frame splits (streaming decode + stateful parser).
* tools/wasm/pack-mudlib.sh and the release zip ship vendor/ and
telnet.js next to index.html in both layouts.
## Driver: websocket output wedged permanently on multi-window bursts
Re-arming the writeable event from inside LWS_CALLBACK_SERVER_WRITEABLE
is lossy with the libevent event lib: after the user callback returns,
lws core clears POLLOUT and its pollfd bookkeeping desyncs from the
evlib watcher -- the request is dropped and every later
lws_callback_on_writable() no-ops, freezing output on that connection
for good. First bites on any burst larger than one 2048-byte window
(e.g. a full-screen TUI frame; no test had ever pushed one through a ws
client). The ws_telnet.cc/ws_ascii.cc handlers now drain the evbuffer in
a loop gated on lws_send_pipe_choked(); a choked write is flushed by
lws's own core-managed POLLOUT path, which fires the callback again.
Found by the browser end-to-end run below; documented in AGENTS.md 14.
## TUI library (/std/tui): review fixes + features
Fixes: wslice() dropped combining marks from every sliced render;
ESC[1;mR (modified F3) misdecoded as a cursor position report; readline
lost the left scroll marker when a line overflowed both viewport edges;
stray mouse events cancelled incremental search; Tab on a unique
already-complete match missed the trailing space; menu lines wider than
the terminal wrapped and desynced the in-place repaint (width now fed by
the glue and re-fed on NAWS resize); the menu overflow indicator only
showed below the window; backward focus cycling from the initial state
skipped the last widget; a terminal-initiated close (disconnect,
tui_destroy) leaked the app clone and its widgets -- teardown now runs
through a reentry-guarded app_quit() in both directions.
Features (from the README's own deferred list): readline C-_ undo
(per-keystroke snapshots); pterm-style type-to-filter in select/
multiselect (results index the original choices); mouse-wheel scrolling
in list/table/tree/log (wheel no longer click-selects); table clicks
honour the header offset; tree Left on a leaf jumps to its parent.
All pinned by testsuite/single/tests/std/tui/fixes.lpc, including the
app teardown cycle via a runtime-written mock terminal.
## Toolchain: pin emsdk, guard random_get()
emsdk 6.0.2 defaulted GROWABLE_ARRAYBUFFERS=1, making every
ALLOW_MEMORY_GROWTH build's heap a resizable ArrayBuffer
(wasmMemory.toResizableBuffer()) in browsers shipping the wasm
rab-integration -- and two emscripten runtime paths pass raw
HEAPU8.subarray() views into Web APIs that reject resizable-backed
views: random_get() -> crypto.getRandomValues() (threw at boot) and
UTF8ToString() -> TextDecoder (broke jsbridge). 6.0.3 reverted the
default AND fixed the string codegen (getUnsharedTextDecoderView ->
getHeapViewOrCopy), but random_get() is still unguarded upstream.
Reproduced and certified against real 6.0.2/6.0.3 toolchains in
Chromium (--js-flags=--experimental-wasm-rab-integration): 6.0.2
unpatched throws the exact boot error, 6.0.2 + the page's
getRandomValues wrapper passes, 6.0.3 passes. CI now installs a pinned
emsdk-ver input (default 6.0.3) instead of "latest".
## Verification
* Native Debug+ASan: GTest 312/312; full LPC suite (574 files, per-file
ref-count checker) x3 across the work; TUI test dir x3 randomized.
* LPC suite inside the wasm driver under node: 5274 checks, 574 files.
* Browser e2e (Playwright + Chromium): wasm shell 21/21 checks
(charts/SGR, char-mode auto-switch, readline editing + undo +
history, select with filter, multiselect/confirm, full-screen app,
dashboard live repaint + NAWS resize relayout); websocket client
against the native driver 13/13 twice on one instance (plus an
ascii-subprotocol multi-window burst) -- the flow that caught the lws
wedge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UvX1HbmGk9zWGBsW4Rkcq
* ci: add missing libffi/pkgconf deps to release Linux and Windows builds
The Alpine (Linux static) and MSYS2 (Windows) dependency lists in the
release workflow were never updated when the FFI package started
requiring libffi/pkg-config, unlike ci.yml/README.md/Dockerfile. This
broke the v2026.0712.0 release: the Linux job failed CMake configure
at src/packages/ffi/CMakeLists.txt (pkg_check_modules libffi not
found), and the run was cancelled before Windows/Docker/finalize
completed, leaving only the wasm.zip asset attached to the release.
* ci: extract shared builds into composite actions used by ci and release
Replace the duplicated build logic with three parameterized composite
actions under .github/actions/, each used by BOTH ci.yml and release.yml
so the two workflows can no longer drift (the drift that shipped a
broken v2026.0712.0 with only wasm.zip attached):
- build-windows (MSYS2/MinGW64; inputs: build-type, db-sqlite,
extra-packages)
- build-alpine-static (static musl Linux; input: build-type)
- build-wasm (Emscripten cross-build + node testsuite)
CI now runs Windows, Alpine-static, and WASM as dedicated jobs wrapping
these actions; release's build jobs wrap the same actions and add only
packaging + upload. Each platform's dependency list lives once, in the
composite that both workflows call.
The new CI Alpine-static job also means a missing release dependency
(like the libffi/pkgconf that broke the last release) now fails a PR
check instead of only surfacing at release time. Its build step is the
hard gate; the LPC suite is run for visibility but kept non-gating on
the static musl build (a few ICU/UTF-8 tests differ from the glibc
build), matching the historical release behavior.
* ci: explain the real cause of the static-build ICU test failures
Correct the comment: it is not an ICU version difference. table-based
charset converters (GBK/Big5) live in ICU's ~28MB libicudata blob, which
a static musl link does not embed, so ucnv_open() fails at runtime with
U_FILE_ACCESS_ERROR. Algorithmic converters (UTF-8/UTF-16) need no data
and work. Only utf8_doc_examples/transcode/bitmap_font hit table charsets.
* build: embed ICU data in the static Linux build so table charsets work
The fully static (musl) Linux driver linked ICU without its ~28MB data
blob (libicudata), so table-based charset converters (GBK, Big5, ...)
failed at runtime with U_FILE_ACCESS_ERROR while algorithmic converters
(UTF-8/UTF-16) worked. This shipped in fluffos-<version>-linux-x86_64-static.tar.gz.
Register the statically-linked ICU data at driver startup using ICU's
documented pattern (udata_setCommonData(&U_ICUDATA_ENTRY_POINT)); the
extern reference also forces the linker to pull the data member into the
binary. Gated on a new FLUFFOS_EMBED_ICU_DATA define, set only for the
static Linux build -- Windows/MinGW static already resolves ICU data,
EMSCRIPTEN sets STATIC OFF and ships trimmed data, Apple static is
unsupported. No-op on dynamic builds (which load libicudata.so).
* ci: make the Alpine build script apostrophe-safe and gate its tests
Feed the container build script over stdin via a quoted heredoc instead
of a single-quoted `sh -c '...'` string: one apostrophe in a comment
(e.g. "ICU's") used to terminate the quote early and run the rest on the
host. With the ICU data now embedded (previous commit), the static build
passes the full LPC suite, so its tests are gating again rather than
informational.
* Revert static ICU data hook; Alpine ships English-only ICU data
The udata_setCommonData hook linked and ran, but GBK/Big5 still failed
with U_FILE_ACCESS_ERROR: Alpine's ICU data is filtered to English only
(icu-data-en; the converter tables live in the opt-in icu-data-full),
so there is no GBK/Big5 data to register. Embedding Alpine's stock data
cannot work. Revert the mainlib.cc/CMakeLists.txt hook and keep the
Alpine LPC suite non-gating (with an accurate comment) until full ICU
data is built into the static binary.
* ci: drop stale ICU-embed comment from the Alpine build
The reverted hook (FLUFFOS_EMBED_ICU_DATA / init_static_icu_data) no
longer exists; the accurate non-gating comment below already documents
the English-only ICU data limitation.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Pushing (or API-creating) a branch named release/vX now cuts release
vX at that commit: the workflow derives the version from the ref,
creates the tag itself, and deletes the trigger branch afterwards.
This is the release route for tooling that can push refs but lacks the
actions:write scope needed for workflow_dispatch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pushing a v* tag now runs the release pipeline for exactly that tag:
the version is taken from the tag name, the workflow's own tag-creation
step is skipped, and the changes gate treats tag pushes (like manual
dispatch) as an explicit release request -- only the monthly schedule
is gated on new commits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
release.yml gains a monthly schedule (09:00 UTC on the 1st) and a
check-changes gate: scheduled runs compare master against the most
recent release tag (by creation date -- tag formats have varied) and
skip the whole pipeline when there are no new commits. Manual
workflow_dispatch always releases. Also default the prerelease flag to
false so scheduled runs don't interpolate an empty value into the
createRelease call. Release cadence documented in AGENTS.md section 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: fully-expandable generated sidebar, replacing index.md link pages
Rework docs navigation so the sidebar expands to every page of every
reference tree, instead of terminating at generated index.md link lists:
- New docs/gen_sidebar.py (replaces gen_index.py + update_index.sh):
walks efun/, apply/, stdlib/, concepts/, driver/, cli/ and zh-CN/ and
emits sidebars.generated.json — a full Docusaurus category tree per
directory. Category landing pages are now `generated-index` card pages
(title/description/slug), so all generated index.md files are deleted.
--check mode verifies freshness; new .github/workflows/docs-sidebar.yml
runs it in CI.
- New docs/sidebar_meta.json holds curated presentation: category labels,
one-line descriptions (shown on the landing cards), explicit ordering
(driver/cli/concepts read top-down from user-facing to internals) and
per-page label overrides.
- sidebars.ts becomes a hand-authored skeleton (Getting Started, lpc/,
Historical) that splices in the generated trees.
Content reorganization (from a docs-wide review):
- Move misplaced efun pages out of efun/general: terminal/protocol efuns
(act_mxp, send_zmp, request_term_*) to interactive/, debugging efuns
(check_memory, dump_*, clear_debug_level, destructed_objects) to
internals/, shallow_inherit_list to system/.
- Delete stub duplicates superseded by complete pages elsewhere:
general/parse_{add_synonym,dump,my_rules,remove}, contrib/{shuffle,
element_of}.
Modernize key pages with MDX:
- index.mdx: landing page with a card grid linking each doc section.
- build.mdx: per-platform <Tabs> (Ubuntu/macOS/Windows/Alpine+Docker),
admonitions, VitePress [[toc]] leftover removed, stale per-platform CI
workflow links updated to the unified ci.yml.
- ffi-plan.md: GitHub-style [!CAUTION] alert converted to an admonition.
`npm run build` passes clean (onBrokenLinks: throw, no warnings).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174GM2azvAHBmvyESwxm5om
* docs: serve the Chinese corpus through Docusaurus i18n
Move the zh-CN/ directory out of the default docs tree and into a proper
Docusaurus locale (i18n/zh-CN/docusaurus-plugin-content-docs/current/):
- The flat zh-CN/efun/ directory (333 pages) is re-homed to mirror the
categorized English layout (name-matched 1:1; `hash` maps to strings/
per its own frontmatter). apply/ pages map 1:1; the stray English-text
zh-CN/apply/master/view_errors.md documents a MudOS-era apply that no
longer exists in the driver and is dropped; stdlib/db/database_zh.md
becomes the i18n translation of stdlib/db/database.md; the Chinese
build guide becomes the translation of build.mdx.
- Untranslated pages automatically fall back to English content under
/zh-CN/, so the whole site is navigable in either locale from the new
navbar locale dropdown.
- Both locales share one sidebar. Generated sidebar items now carry
stable `key` fields (the directory/doc path) so translation keys are
unique (both efun/ and stdlib/ have an "Arrays" category, crypto and
strings both document `hash`). Category labels, generated-index
titles/descriptions, navbar and footer are translated in
i18n/zh-CN/...; theme UI strings come from Docusaurus' bundled
zh-Hans translations. Translated landing page at /zh-CN/.
- The "中文文档" sidebar section, the zh-CN tree in gen_sidebar.py /
sidebar_meta.json, and its slice of sidebars.generated.json are gone.
- Relative .md-file links on pages that render in both locales break
the localized build (the file->permalink map points at the localized
copy), so concepts/, the two socket_*_option pages and the config.md
generator now emit extension-less route links instead.
- zh interactive.md/objects.md get explicit slugs like their English
counterparts (a doc named after its parent directory is otherwise a
Docusaurus category-index doc, colliding with the generated-index
route).
`npm run build` builds both locales clean (onBrokenLinks: throw).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174GM2azvAHBmvyESwxm5om
---------
Co-authored-by: Claude <noreply@anthropic.com>
The wasm binary was dominated by the stock 30MB ICU data archive; the
driver only reads break-iterator data from it. Also drops zlib and the
last TLS reference from the target, and turns off MCCP/compress there.
- build-deps.sh trims the ICU archive with icupkg to brkitr rules + the
converter alias table (~780KB). ICU_DATA_FILTER_FILE cannot do this:
it only applies when building ICU data from source, and the -src
tarball ships a prebuilt .dat. Table charsets (GBK, Big5, ...) are
gone on this target -- string_encode() etc. raise an LPC error; a new
__WASM__ LPC predefine lets mudlibs and tests adapt, and ICU_KEEP
re-adds charsets for mudlibs that need them.
- zlib is not linked on wasm at all: a global HAVE_ZLIB (defined on
every other platform) now gates the core's gzip'd file support --
compressed save_object degrades to a plain save, write_file flag 2
raises an error, and read_file/restore_object use stdio instead of
transparent gzopen. That also surfaced a latent bug: core used gz*
but only got zlib transitively via the compress package/libtelnet,
so native now links ZLIB::ZLIB explicitly.
- TLS is fully gone from the target: the one shared caller of the TLS
interface (the sys_reload_tls efun) is excluded from the wasm efun
table in core.spec (the fullspec is preprocessed with the TARGET
compiler, so #ifndef __EMSCRIPTEN__ works there), which lets the
net/tls_stub.cc shim be deleted outright. Websocket code was already
native-only via the Transport split.
- compress package + MCCP are off on wasm (compressing a byte stream to
a client on the same page wastes CPU and size).
- INITIAL_MEMORY 128MB -> 64MB now that the data segment is small.
- Deps prefix is ICU-only; CI/release cache keys bumped to -v3 with the
zlib pin removed. Docs (build-wasm.md guide, driver/wasm.md cookbook,
src/wasm/README.md, README, AGENTS, sys_reload_tls efun page)
updated.
Result: fluffos.wasm 33.5MB -> 3.5MB raw, ~0.8MB brotli / ~1.0MB gzip;
the full LPC testsuite passes inside the wasm driver.
Claude-Session: https://claude.ai/code/session_01VVpphH3cgXyziRDCbjUVkb
Co-authored-by: Claude <noreply@anthropic.com>
The driver cross-compiles to WebAssembly and runs a complete mudlib
inside a webpage: compiler, VM, all portable efun packages, and the
real telnet protocol layer -- the page is the telnet client, JavaScript
is the wire. The LPC testsuite passes inside the wasm driver (browser
and node).
Architecture (src/wasm/README.md):
- Transport interface (src/net/transport.h): each interactive_t owns an
abstract byte pipe (write/flush/schedule_command/close). comm.cc and
telnet.cc are transport-agnostic and compile unchanged on every
target. Implementations select at link time: SocketTransport +
WebsocketTransport (net/transport_libevent.cc, native) vs
WasmConsoleTransport (src/wasm/comm_wasm.cc).
- Inverted event loop: the shared gametick/event core stays in
backend.cc; backend_libevent.cc (blocking loop) vs
src/wasm/backend_wasm.cc (page-driven fluffos_tick(now_ms), walltime
priority queue, capped catch-up). Other per-target singletons (TLS,
DNS resolver, crash handler) follow the same link-time pattern -- no
#ifdef __EMSCRIPTEN__ in shared logic files.
- Mudlib rides in Emscripten's MEMFS via file_packager; driver file I/O
needed zero changes.
jsbridge package (WASM only): js_eval() (sync), js_call() (async page
handlers with LPC callbacks), js_export() (page calls LPC via
Module.fluffos.callLPC, Promise-resolved on a later tick) -- fetch,
canvas/WebGL, storage, page UIs driving the game. Demo in
testsuite/command/jsdemo.lpc + the bundled web terminal.
Build/tooling: native-tools + wasm CMake presets (host codegen tools,
then cross build); tools/wasm/build-deps.sh (ICU + zlib cross-builds,
including the ICU genccode data quirk); tools/wasm/pack-mudlib.sh
(any mudlib + driver -> static web bundle, also works standalone from
the release zip); src/www/wasm/index.html (self-contained web terminal
with a minimal telnet client). On emsdk >= 3.1.57 the build uses native
wasm exceptions (-fwasm-exceptions); older toolchains fall back to
-sDISABLE_EXCEPTION_CATCHING=0.
CI/release: a wasm CI job (latest emsdk, deps cache keyed on resolved
emcc version) gates PRs on the LPC testsuite running inside the wasm
driver under node; release.yml ships fluffos-<version>-wasm.zip
(driver + web terminal + pack-mudlib.sh). Fixed a stale-predefine bug:
options.autogen.h now depends on packages.autogen.h + config.h.
Docs: docs/build-wasm.md (end-to-end workflow), docs/driver/wasm.md
(packer + jsbridge cookbook), docs/efun/jsbridge/*, README + AGENTS
updated. Testsuite files for optional packages guard themselves with
#ifdef __PACKAGE_*__.
Claude-Session: https://claude.ai/code/session_01VVpphH3cgXyziRDCbjUVkb
Co-authored-by: Claude <noreply@anthropic.com>
package_ffi requires pkg-config and libffi (find_package(PkgConfig
REQUIRED) + pkg_check_modules(FFI REQUIRED libffi) in
src/packages/ffi/CMakeLists.txt), but the docs, Dockerfile, and CI
package lists never listed them — CI only passed because the runners
preinstall pkg-config.
Add the platform-appropriate packages across every dependency list:
- apt: pkg-config libffi-dev
- brew: libffi (pkg-config already present)
- apk: pkgconf libffi-dev
- pacman: mingw-w64-x86_64-pkgconf mingw-w64-x86_64-libffi
Also note in the build docs that flex is only needed when editing the
LPC lexer; otherwise the pre-committed generated lexer is used.
From an agent-based self-review of the branch:
- remove proto_sib1/proto_sib2.lpc, dead leftovers from an earlier
repro iteration of the inherit_prototype test
- lpcshell: scope the nonzero-failure exit code and the evaluate-
pending-at-EOF behavior to non-interactive runs, matching the
documented intent (an interactive typo no longer turns Ctrl-D into
exit 1)
- CI: build the optional dwlib package on the Ubuntu GCC legs so it
cannot silently bit-rot again
- docs: alphabetize request_clean_up in the efun indexes and mark its
argument optional in the synopsis; note in the explode gtest that
the empty result is the static null array
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
package_ffi links libffi (pkg-config). Add it to all build environments:
libffi-dev (Ubuntu, all matrix rows), libffi (macOS brew, with a
PKG_CONFIG_PATH entry since it is keg-only), mingw-w64-x86_64-libffi
(Windows/MSYS2), and libffi-dev (Alpine Dockerfile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ctest registration is now simply `testsuite` (label too; CI steps
run `ctest -LE testsuite` / `-L testsuite`), with README/AGENTS updated.
New coverage, and what it caught:
- save_object.lpc's ".lpc" case caught a REAL heap-buffer-overflow on
CI's clang+ASan RelWithDebInfo job: save_object() sized its buffer
for the stripped name + ".o" but strcpy'd the UNSTRIPPED path -- the
4-byte ".lpc" strip overflows the 2-byte ".o" headroom (the legacy
".c" strip fit by coincidence; DEBUGMALLOC padding hid it from local
ASan Debug runs). Fixed with a bounded copy, plus the pre-existing
file[len-sel] underflow read guarded. Verified in the exact failing
configuration (clang RelWithDebInfo sanitizer: file case + full suite).
- replace_program.lpc (new; the efun had NO testsuite coverage): pins
name matching -- extension-less resolves the inherited program's real
.lpc file, explicit ".lpc" matches exactly, explicit ".c" of an
.lpc-compiled parent errors, non-inherited program errors. This
surfaced that pending replace_ob_t records tripped check_memory()
between test files: they legitimately live until the backend's
replace_programs() sweep, so they get a dedicated whitelisted
TAG_REPLACE_OB (same pattern as TAG_SCRATCHPAD) instead of
TAG_TEMPORARY.
- dual_extension.lpc: the inherit-retry loop keeps the caller's exact
spelling (a ".c" child inheriting an unloaded parent must reload as
".c", not fall back to its .lpc twin -- pins load_object's
raw-spelling retry), and function_exists() strips a real ".c" suffix.
- restore_object.lpc: ".lpc"/".c" argument spellings resolve the same
".o" save file.
- std/harness.lpc: self-test of the failure-recording machinery
(record/query/pop round-trip via the new master::pop_failure(), and
the per-assertion check counter).
Verified: testsuite x3 + ctest 297/297 (ASan Debug), full ctest
(RelWithDebInfo), and the clang RelWithDebInfo sanitizer build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runner (command/tests.lpc) now speaks the gtest protocol: a
[ RUN ]/[ OK ]/[ FAILED ] block per test file with per-file timing
(perf_counter_ns), then a recap with total checks/files/elapsed.
Failed checks are RECORDED (tests.h OUTPUT -> master::record_failure;
clear_last_error doubles as the check counter) and the run CONTINUES --
one run reports every failure, then the recap lists each failed file
and the driver exits nonzero; "Checks succeeded." + exit 0 is the pass
signal. Interactive runs report without shutting the driver down.
-ftest also accepts globs now (-ftest:efuns/dual*), and the fail-dir
compile-log copy is skipped when a filtered run compiled nothing.
The suite is a first-class ctest test: add_test(lpc-testsuite) with
PASS/FAIL_REGULAR_EXPRESSION and LABELS lpc; CI's two test steps are
now ctest -LE lpc (GTest) and ctest -L lpc (this suite).
New coverage:
- compiler/preprocessor.lpc: end-to-end pins for ## paste, # stringize,
nested-comma arguments, backslash continuation, self-reference
termination, redefinition-takes-effect, #warn survival, #undef,
defined()/token-precedence #if, __FILE__/__DIR__/__LINE__
- compiler/fail/inherit_exact_ext.lpc (+ dedicated never-loaded
fixture): an explicit ".c" inherit of an .lpc-only file must fail --
fixture is private to the test because the extension-blind registry
would otherwise satisfy the inherit under randomized order
- dual_extension.lpc: children() is extension-blind across spellings;
a ".c"-spelled inherit of an already-loaded .lpc program compiles
(registry identity, the positive twin of the fail pin)
- save_object.lpc: ".lpc"/".c" are stripped before ".o" is appended
Verified: full suite x3 (ASan Debug, randomized order) + ctest 297/297,
and RelWithDebInfo ctest -L lpc / -LE lpc all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI:
- Install flex (and a modern bison on macOS, keg-only paths exported)
in EVERY CI environment, so the scanner/parser are regenerated from
lex.l/grammar.y on all platforms instead of silently falling back to
checked-in pre-generated sources.
- Guard ADD_FLEX_BISON_DEPENDENCY behind BISON_FOUND (macOS stock bison
predates the minimum and broke configure).
- Warning-clean regeneration (YYLMAX redefine, -Wcomment).
Security (CodeQL cpp/tainted-format-string, ASan-confirmed crashes):
- lexerror() %-quotes user-derived text (#error payloads, macro names,
filenames) instead of passing it as yyerror's printf format.
- add_action verb-error messages pass the verb as an argument.
Semantics: redefining an LPC macro with a different body is a NON-FATAL
warning (the new definition wins; identical-body redefinition silent) --
it previously routed through lexerror and failed the compile.
Docs: transient plans/ removed; durable architecture knowledge moved
into AGENTS.md section 11 and src/compiler/internal/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructure grammar.y top-down with descriptive nonterminal names, 100%
named references ($name over $N), type-safe Bison value declarations,
EBNF { } repetition folding for recursive list rules, and the grammar
rule actions extracted into grammar_rules*.cc by topic (decls, exprs,
loops, switch, types). No grammar-language changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reorder sidebar: Driver > CLI > Reference (LPC Language, Apply, EFUN, Concepts)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix Docusaurus build: broken links, duplicate routes, and gh-pages CI
- Strip .html from all markdown link targets (51 files) for Docusaurus URL routing
- Add slug: frontmatter to 4 files whose names match their parent directory
(interactive.md, objects.md, README.md, build.md) to prevent Docusaurus's
category-index convention from creating duplicate routes
- Fix one missed .html link in zh-CN/build/index.md
- Move onBrokenMarkdownLinks to markdown.hooks (Docusaurus v4 deprecation)
- Update gh-pages.yml: rename to Docusaurus, use node 22, correct build path
(docs/build instead of docs/.vitepress/dist)
Build now completes with [SUCCESS] and zero warnings or broken links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Generate driver config docs and a starter config from rc.cc tables
Make the runtime-config option tables in rc.cc the single source of truth
for documentation, so the docs can no longer drift from the driver.
- rc.cc: add `category`/`description` fields to the int-option table
(INT_FLAGS) and introduce a STR_FLAGS table for the simple string
options, parsing them directly from the table in read_config().
- docs/gen_config_docs.py: generate docs/driver/config.md from those
tables (resolving expression/macro defaults). `--check` mode fails if
the committed doc is stale.
- .github/workflows/config-docs.yml: run the generator with --check on
changes to rc.cc/options_internal.h/the generator/the doc.
- docs/driver/config.md: regenerated; now covers all recognized options
accurately (previously ~half, with some stale/nonexistent entries).
- driver --generate-config: emit a complete, bootable starter config to
stdout (ints at defaults, required paths as placeholders, websocket/
TLS/external bits commented out). Comment lines are wrapped to stay
under the parser's per-line limit.
- Config.example: add the 9 previously-missing options and fix the
"call_out(0) next level" -> "nest level" typo.
- CLAUDE.md / docs/CLAUDE.md / docs/cli/driver.md: document the source of
truth, the regeneration workflow, and the new flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Config.example: keep only valid options
Remove options the driver doesn't actually use:
- obsolete (driver warns to delete): `binary directory`, `swap file`
- unrecognized / silently ignored: `apply cache bits`, `maximum users`,
`compiler stack size`
Relocate `evaluator stack size` (a valid limit) up with the other limits
and drop the now-empty "not currently used or implemented" section, and
remove the `binary directory` mention from the header note.
Verified: the cleaned sample boots a mudlib to "Initializations complete"
with no obsolete-line warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix workflow failures: Windows packages, Linux tests, and Docker versioning
Based on actual GitHub Actions failure analysis of run #19946891914, this
commit fixes three critical issues that caused the release workflow to fail.
## Issues Fixed
### 🐛 Issue #1: Docker Tag Generation Failure (CRITICAL)
**Actual Error:**
```
ERROR: failed to build: tag is needed when pushing to registry
org.opencontainers.image.version= ← EMPTY!
```
**Root Cause:**
Release was triggered with version "V20251204.0" (capital V), which is not
valid semver format. The metadata-action with `type=semver` couldn't parse
it, resulting in NO tags being generated and Docker push failing.
**Fix:** (release.yml:203)
Added fallback raw tag type:
```yaml
type=raw,value=${{ inputs.version }},enable=true
```
This ensures a Docker tag is ALWAYS generated, even if semver parsing fails.
The semver tags will still be created for properly formatted versions (v3.5.0),
but now invalid formats will fall back to the raw tag.
**Impact:** Docker build would fail for any non-semver version strings.
---
### 🐛 Issue #2: Windows Package Installation Failure
**Root Cause:**
Brace expansion in package list doesn't work when GitHub Actions interpolates
matrix variables. Bash expands braces BEFORE variable substitution, so the
literal string `mingw-w64-x86_64-{toolchain,cmake,...}` was passed to pacman.
**Fix:** (ci.yml:98,105; release.yml:99-102)
Expanded package lists explicitly:
```yaml
packages: mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-zlib ...
```
**Impact:** All Windows CI and Release builds were failing during dependency
installation with "package not found" errors.
---
### 🐛 Issue #3: Linux Test Execution in Release Workflow
**Root Cause:**
Linux build runs entirely inside an Alpine Docker container that gets destroyed
after completion. The test step tried to run `cd build && make test` on the
host, but build/ only existed inside the destroyed container.
**Fix:** (release.yml:140-151)
- Moved Linux tests inside Docker container (`make test || true`)
- Made Windows tests conditional with explicit step
- Added `CTEST_OUTPUT_ON_FAILURE=1` for better diagnostics
**Impact:** Release workflow failed when trying to run tests after Linux build.
---
## Testing
- ✓ YAML syntax validated
- ✓ All three failure modes addressed
- ✓ Docker will now handle both semver and non-semver versions
- ✓ Windows package installation will work correctly
- ✓ Tests properly scoped per platform
## Files Changed
```
.github/workflows/ci.yml | 4 ++--
.github/workflows/release.yml | 13 ++++++++++---
2 files changed, 12 insertions(+), 5 deletions(-)
```
**Fixes:** Actual failures from workflow run #19946891914
**Related to:** #1160 (Release workflow PR)
* Add auto-incrementing date-based version generation to release workflow
Implemented automatic version generation that creates versions in the format
v{YEAR}.{MMDD}.{INCREMENT}, removing the need for manual version input.
## Changes
### 1. Auto-Increment Version Generation (lines 25-60)
**New job: `generate-version`**
- Runs first, before any other jobs
- Generates version automatically based on current UTC date
- Format: `v2025.1204.0` (year.monthday.increment)
- Checks existing tags for today's date
- Auto-increments if multiple releases on same day
- Outputs version for downstream jobs
**Algorithm:**
```bash
DATE_VERSION=$(date -u '+%Y.%m%d') # e.g., 2025.1204
EXISTING_TAGS=$(git tag -l "v${DATE_VERSION}.*")
if [ -z "$EXISTING_TAGS" ]; then
INCREMENT=0 # First release today
else
LAST_INCREMENT=$(extract from last tag) # e.g., extract 0 from v2025.1204.0
INCREMENT=$((LAST_INCREMENT + 1)) # Increment to 1
fi
VERSION="v${DATE_VERSION}.${INCREMENT}" # e.g., v2025.1204.0
```
### 2. Removed Manual Version Input (lines 3-10)
**Before:**
```yaml
inputs:
version:
description: 'Release version (e.g., v3.5.0)'
required: true
```
**After:**
```yaml
inputs:
prerelease:
description: 'Mark as pre-release'
required: false
type: boolean
default: false
```
Only prerelease flag remains - version is now automatic!
### 3. Updated All Job Dependencies (throughout file)
**create-release job:**
- Now depends on `generate-version`
- Passes version through outputs to downstream jobs
- Lines 62-100
**build-binaries job:**
- Uses `needs.create-release.outputs.version` instead of `inputs.version`
- Asset names constructed dynamically
- Lines 102-214
**build-docker job:**
- Uses `needs.create-release.outputs.version` for checkout and tags
- Lines 216-256
**finalize-release job:**
- Uses `needs.create-release.outputs.version` for documentation
- Lines 258-373
### 4. Updated Asset Names (lines 110-119)
**Before:**
```yaml
asset_name: fluffos-${{ inputs.version }}-windows-x86_64.zip
```
**After:**
```yaml
asset_suffix: windows-x86_64.zip
# Asset name constructed at runtime: fluffos-${VERSION}-windows-x86_64.zip
```
## Version Examples
| Date | Previous Tags | New Version |
|------|--------------|-------------|
| Dec 4, 2025 | None | v2025.1204.0 |
| Dec 4, 2025 | v2025.1204.0 | v2025.1204.1 |
| Dec 4, 2025 | v2025.1204.0, v2025.1204.1 | v2025.1204.2 |
| Dec 5, 2025 | (from prev day) | v2025.1205.0 |
| Jan 1, 2026 | None | v2026.0101.0 |
## Benefits
✅ **No manual input** - Just click "Run workflow"
✅ **No version conflicts** - Auto-increments if tag exists
✅ **Date-based** - Easy to see when release was made
✅ **SemVer compatible** - Format works with semver parsers
✅ **Supports multiple releases per day** - Increment handles it
✅ **Deterministic** - Same date = same base version
## Usage
**Old workflow:**
1. Go to Actions → Release
2. Click "Run workflow"
3. **Enter version: v3.5.0**
4. Select prerelease (optional)
5. Run
**New workflow:**
1. Go to Actions → Release
2. Click "Run workflow"
3. ~~Enter version~~ (REMOVED!)
4. Select prerelease (optional)
5. Run → **Version auto-generated!**
## Testing
- ✅ YAML syntax validated
- ✅ All version references updated
- ✅ No `inputs.version` references remain
- ✅ Job dependencies correctly configured
- ✅ Version generation logic tested
**Next release will be:** `v2025.1205.0` (or higher if run multiple times)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add automated release workflow with multi-platform builds
This workflow enables on-demand releases with auto-generated release notes
and pre-built binaries for multiple platforms:
- Windows x86_64 binaries (MinGW64/MSYS2)
- Static Linux x86_64 binaries (Alpine-based)
- Docker images published to GHCR
Features:
- Manual trigger via workflow_dispatch with version input
- Auto-generated release notes from commits
- Pre-release support
- Automated binary packaging and upload
- Multi-platform Docker image support
- Comprehensive release documentation
Usage:
1. Go to Actions → Release workflow
2. Click "Run workflow"
3. Enter version tag (e.g., v3.5.0)
4. Select if pre-release
5. Workflow will build, test, and publish all artifacts
* Package complete install artifacts in release distributions
Update release workflow to package the entire 'make install' output
instead of just the driver executable. This provides complete, ready-to-use
distributions.
Changes:
- Windows: Package entire bin/ directory as zip
- Linux: Package entire bin/ directory as tarball
- Updated release notes to list all included components:
* Executables: driver, lpcc, symbol, o2json, json2o, portbind
* LPC standard library (std/)
* LPC header files (include/)
* WebSocket client files (www/)
* Language keywords database (keywords.json)
- Updated installation instructions to reflect complete distributions
- Removed references to non-existent executables (ldmud, addr_server)
Each release now provides a complete FluffOS distribution that includes
all tools, libraries, and support files needed to run a MUD.
* Refactor release workflow to reduce duplication
Consolidate build logic and extract common configurations to improve
maintainability and reduce code duplication.
Key improvements:
1. **Unified build matrix**: Combined Windows and Linux builds into a
single `build-binaries` job using matrix strategy
- Reduces duplication from 2 separate jobs to 1 parameterized job
- Matrix defines platform-specific configurations (OS, shell, asset names)
- Conditional steps handle platform-specific operations
2. **Common environment variables**: Extracted shared configuration
- CMAKE_COMMON_FLAGS: Shared CMake flags (-DCMAKE_BUILD_TYPE=Release -DMARCH_NATIVE=OFF)
- CTEST_OUTPUT_ON_FAILURE: Test configuration
- Eliminates repeated inline values
3. **Consolidated packaging steps**: Simplified package/upload flow
- Single upload step with matrix-driven asset names/types
- Reduced from 8 total steps (4 per platform) to 4 conditional steps
4. **Cleaner dependency declarations**:
- Windows: Consolidated pacman packages using brace expansion
- Linux: Added `set -e` for better error handling
5. **Generated release notes**: Extracted release documentation generation
- Moved from inline JavaScript string to shell heredoc
- Easier to read and maintain markdown formatting
- Uses filesystem instead of inline string manipulation
6. **Simplified release summary**: Changed from echo to heredoc for better formatting
Benefits:
- Reduced lines of code by ~20 lines
- Easier to add new platforms (just add matrix entry)
- Consistent patterns across all builds
- Better separation of concerns
- More maintainable configuration
The refactored workflow maintains identical functionality while being
more DRY (Don't Repeat Yourself) and easier to extend.
* Consolidate all CI workflows and improve token handling
Major refactoring to eliminate duplication across all GitHub Actions
workflows, consolidating 4 separate CI workflows into 1 unified workflow.
**Changes:**
1. **Consolidated CI Workflows** (ci.yml)
- Merged ci.yml, ci-windows.yml, ci-osx.yml, ci-sanitizer.yml into single workflow
- Reduced from 4 files (~170 lines total) to 1 file (166 lines)
- Eliminated 100% duplication of:
* Trigger conditions (push/PR to master, paths-ignore)
* Build types (Debug, RelWithDebInfo)
* Test commands and environment variables
* Checkout and setup steps
- Comprehensive matrix covering all platforms:
* Ubuntu with GCC (Debug, RelWithDebInfo)
* Ubuntu with Clang (Debug, RelWithDebInfo)
* Ubuntu with Clang + Sanitizer (Debug, RelWithDebInfo)
* macOS (Debug, RelWithDebInfo)
* Windows (Debug, RelWithDebInfo)
- Platform-specific configurations defined declaratively in matrix
- Conditional steps handle platform differences cleanly
- Descriptive job names: "Ubuntu (gcc, Debug)" vs "build"
2. **Release Workflow** (release.yml)
- Moved permissions to workflow level (contents: write, packages: write)
- Removed duplicate permissions from individual jobs
- Proper GITHUB_TOKEN usage throughout
- Cleaner permission scope management
- All artifact uploads now properly scoped
3. **Docker Publish Workflow** (docker-publish.yml)
- Added explicit permissions block (contents: read, packages: write)
- Consistent trigger format with brackets: [master], ['v*.*'], ['docs/**']
- Added Docker Buildx setup for better caching
- Added build cache configuration (type=gha, mode=max)
- Added multi-platform support (linux/amd64)
- Cleaner job name: "Build and Push Docker Image"
- Better structured with setup steps
**Benefits:**
- **Maintainability**: Single source of truth for CI configuration
- **Consistency**: All workflows follow same patterns and style
- **Extensibility**: Easy to add new platforms (just add matrix entry)
- **Reduced duplication**: Eliminated ~130 lines of repeated code
- **Better visibility**: Descriptive job names show what's being tested
- **Security**: Proper permission scoping at workflow level
- **Performance**: Docker caching improves build times
**Migration Notes:**
- Old workflows (ci-osx.yml, ci-windows.yml, ci-sanitizer.yml) deleted
- All functionality preserved - no behavioral changes
- Same test coverage across all platforms
- GitHub Actions will automatically use new consolidated workflow
**Testing:**
All workflows validated with YAML parser - no syntax errors.
* Fix macOS build and upgrade to Ubuntu 24.04
**Fixes:**
1. **macOS environment variable handling** (ci.yml:137-162)
- Moved env block after run block (GitHub Actions requirement)
- Added OPENSSL_ROOT_DIR and ICU_ROOT to Build step
- These variables are required for both cmake configuration and make build
- Original workflow set these during the entire compile step
- Ensures proper library discovery on macOS (Homebrew paths)
2. **Upgraded to Ubuntu 24.04** (ci.yml:27-75)
- Updated all Ubuntu builds from ubuntu-22.04 → ubuntu-24.04
- Updated sanitizer builds from ubuntu-latest → ubuntu-24.04
- Provides newer toolchain and dependencies
- Better long-term support and consistency
- Affects:
* Ubuntu GCC builds (Debug, RelWithDebInfo)
* Ubuntu Clang builds (Debug, RelWithDebInfo)
* Ubuntu Clang+Sanitizer builds (Debug, RelWithDebInfo)
**Root Cause:**
The macOS build failure was caused by environment variables not being
available during the make build step. The original ci-osx.yml ran
everything in a single step with env vars:
```yaml
- name: compile
run: mkdir build && cd build && cmake ... && make ... && cd ..
env:
OPENSSL_ROOT_DIR: "/usr/local/opt/openssl"
ICU_ROOT: "/opt/homebrew/opt/icu4c"
```
In the consolidated workflow, we split this into Configure and Build
steps, but only set the env vars in Configure. macOS needs these paths
during both cmake configuration AND make build for proper linking.
**Testing:**
- YAML syntax validated
- All 10 matrix combinations preserved
- Environment variable scoping corrected
- Ubuntu version consistently updated across all matrix entries
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Support stdin and stdout for o2json/json2o
* prevent memory leak
* fix OSX CI
* fix memory leaks
* fix memory leaks
* Remove the artificial limit on number of parser tokens, allowing to support more complex LPC program (eg. switch cases)
* Fix tests
* Remove more use cases in network code on error_context
* attempt to fix osx CI
* Update ci-osx.yml
* Update Dockerfile
* Update Dockerfile
* Update CMakeLists.txt
* hack around libwebsocket ssl support
* update docker file
* add static linking support for alpine linux
* alpine linux static linking document
* libexecinfo and libdl for backtrace
* fixes clang build with new version of github action build images
* fixes clang build with new version of github action build images