Commit graph

288 commits

Author SHA1 Message Date
dependabot[bot]
6cf257cedb
build(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#1346)
Bumps the npm_and_yarn group with 2 updates in the /docs directory: [fast-uri](https://github.com/fastify/fast-uri) and [js-yaml](https://github.com/nodeca/js-yaml).


Updates `fast-uri` from 3.1.4 to 3.1.5
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

Updates `js-yaml` from 3.14.2 to 3.15.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.1)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 3.15.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-12 11:48:48 -07:00
dependabot[bot]
2024182d68
build(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#1340)
Bumps the npm_and_yarn group with 3 updates in the /docs directory: [brace-expansion](https://github.com/juliangruber/brace-expansion), [postcss](https://github.com/postcss/postcss) and [undici](https://github.com/nodejs/undici).


Updates `brace-expansion` from 1.1.15 to 1.1.18
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.15...v1.1.18)

Updates `postcss` from 8.5.15 to 8.5.25
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.25)

Updates `undici` from 7.28.0 to 7.29.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.28.0...v7.29.0)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 7.29.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 15:45:23 -07:00
gesslar
e059c891cb
docs: document the real rules for throw(), add a structured catch() example (#1335)
* docs: document the real rules for throw(), add a structured catch() example

throw.md called throw() "forces an error to occur in an object" and
treated the catch() requirement as a style suggestion. Neither matches
what f_throw()/throw_error() do. Rewrite the page around the rules the
driver actually implements, each verified against a running driver:

- An uncaught throw() raises *Throw with no catch. and discards the
  value. This is a requirement, not advice.
- Any type round-trips through catch() verbatim, not just strings.
- throw(0) is indistinguishable from success, since catch() returns 0
  for "no error". This was documented only on catch.md.
- A thrown value skips the error machinery entirely: no traceback, no
  debug log, no error_handler() apply, and no leading '*'. That '*' is
  the discriminator handling code keys on, and it appeared on neither
  page.
- Only the innermost catch() sees it; rethrow to propagate further.
- It crosses ordinary calls (call_other, inherited functions,
  evaluate(), filter/sort_array callbacks, a create() running under
  load_object) but cannot escape a call the driver starts itself
  (call_out, input_to, driver applies), which each begin a fresh chain
  with no catch above them.

The old example's `return;` after throw() was unreachable, and it
concatenated a caught driver error into a new message, burying the '*'
mid-string so callers testing err[0] would stop recognising it. Both
fixed.

catch.md gains a third example that throws a class: its existing two
both use string errors and imply catch() only ever yields text. Written
with dot accessors and named-argument new(), matching the ordering in
lpc/types/classes.md.

Chinese translations updated to match. The zh-CN catch.md description
said throw() returns a non-zero value, narrower than "any value except
0" and the exact claim the new example rests on; corrected.

Every example block was extracted from the markdown and compiled by the
driver, Chinese comments and strings included. Both locales build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JuB4acdTho1rmAL7PwvS1

* docs: fix the rethrow example forging a driver-error '*'

Review feedback on the rethrow example was right, and the bug is worse
than a style nit: the example contradicted the rule its own page
teaches. It stripped a leading '*' from the caught value and then
unconditionally put one back, so anything that was not a driver error
came back disguised as one. Verified against the driver:

  mudlib string -> "*move_or_fail(): took too long\n"   (forged '*')
  structured    -> "*Bad type argument to +. Had string and array\n"

The second case is worse still: concatenating a string onto a thrown
array destroys the original failure and replaces it with a type error
raised inside the handler.

Re-add the '*' only when it was there to begin with, add context
without it for a non-driver string, and pass a non-string value through
untouched. Now:

  driver error  -> "*move_or_fail(): bad thing\n"
  mudlib string -> "move_or_fail(): took too long\n"
  structured    -> ({ "insufficient_funds", 7 })

Also quote the uncaught-throw error as the full string it actually is,
"*Throw with no catch.\n", matching the two example strings shown a few
lines above that spell out their trailing newline.

And drop the "any value, except 0" phrasing on catch.md, which reads as
though throwing 0 were rejected. It is not rejected, merely undetectable,
since catch() already returns 0 for "no error". The zh-CN page inherited
the same ambiguity from the English; both now say so explicitly.

All four example blocks recompiled from the markdown; both locales build
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JuB4acdTho1rmAL7PwvS1

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:08:29 -07:00
gesslar
5f7fba054f
docs: document the third argument (flag) of function_exists() (#1326)
The EN page only showed the two-argument form. Document the optional
third argument, which admits protected and private functions when
nonzero (matching the implementation in interpret.cc and the existing
zh-CN page), and fix the SYNOPSYS -> SYNOPSIS heading typo.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:56:30 -07:00
gesslar
7bbedece7c
docs: document that shuffle() reorders its argument array in place (#1332)
The shuffle.md page never mentioned the most important behavior: the
array passed in is itself mutated (in-place Fisher-Yates in
f_shuffle), and the "return value" is that same array left on the
stack, not a shuffled copy. The zh-CN translation already warned
about this; the English page did not.

Rewrite the page with the mutation front and center, a RETURN VALUE
section, an example showing the shared-array effect plus the
shuffle(a[0..]) copy idiom, and a SEE ALSO in the standard name(3)
format.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:56:08 -07:00
gesslar
4d4ad6d9ce
docs: document the third argument (scroll_lines) of ed_start() (#1328)
The doc page only showed the two-argument form, but the efun spec
(core.spec) and f_ed_start() accept an optional third argument that
sets the number of lines used by the editor's scrolling commands
(default 20). Also document the two-argument disambiguation: a second
argument of 1 is treated as 'restricted', anything else as
'scroll_lines'. The zh-CN page is a verbatim copy of the EN page, so
it receives the same update.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:05:26 -07:00
dependabot[bot]
6436410c83
build(deps): bump the npm_and_yarn group across 1 directory with 4 updates (#1321)
Bumps the npm_and_yarn group with 4 updates in the /docs directory: [body-parser](https://github.com/expressjs/body-parser), [fast-uri](https://github.com/fastify/fast-uri), [shell-quote](https://github.com/ljharb/shell-quote) and [webpack-dev-server](https://github.com/webpack/webpack-dev-server).


Updates `body-parser` from 1.20.5 to 1.20.6
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.5...1.20.6)

Updates `fast-uri` from 3.1.2 to 3.1.4
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4)

Updates `shell-quote` from 1.8.4 to 1.10.0
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/shell-quote/compare/v1.8.4...v1.10.0)

Updates `webpack-dev-server` from 5.2.5 to 5.2.6
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/v5.2.6/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.5...v5.2.6)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 1.20.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: shell-quote
  dependency-version: 1.10.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: webpack-dev-server
  dependency-version: 5.2.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-26 10:42:03 -07:00
dependabot[bot]
719f3e357b
build(deps): bump svgo (#1315)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [svgo](https://github.com/svg/svgo).


Updates `svgo` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v3.3.3...v3.3.4)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 3.3.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-26 10:34:55 -07:00
Yucong Sun
0096a6e70c feat(wasm): enable the pcre package on the WebAssembly target
Cross-build classic libpcre 8.45 (the library src/packages/pcre links)
with emconfigure/emmake into the wasm-deps prefix and stop forcing
PACKAGE_PCRE off under EMSCRIPTEN, so all pcre_* efuns exist in the
browser driver. Mudlibs whose boot-critical code (e.g. simul_efuns
doing ANSI handling with pcre_replace) calls these no longer fail to
boot on wasm.

- tools/wasm/build-deps.sh: new PCRE section following the ICU
  pattern (idempotent re-run guard extended, PCRE_VER override).
  Static, UTF-8 + Unicode properties on (the driver compiles every
  pattern with PCRE_UTF8), JIT off (no executable pages in wasm),
  default chartables (no host-run dftables needed).
- src/CMakeLists.txt: drop the forced PACKAGE_PCRE OFF; FindPCRE
  locates the static lib through CMAKE_FIND_ROOT_PATH -> wasm-deps.
- .github/actions/build-wasm: pcre-ver input feeds the deps cache key
  (old ICU-only caches no longer match) and PCRE_VER reaches
  build-deps.sh.
- docs/build-wasm.md, src/wasm/README.md: pcre moved out of the
  absent-package lists; deps/build notes updated.

Verified: wasm LPC testsuite passes with the 9 pcre efun tests now
active (621 OK / 0 failed, same as native); native rebuild + testsuite
unaffected. fluffos.wasm grows 3,390,869 -> 3,604,494 bytes (+209KB
raw, ~0.84MB brotli over the wire).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyQCUoTo1Z93Py9aVFHQi1
2026-07-23 23:58:50 -07:00
Yucong Sun
3bfb7483fe fix(lpc-syntax): refuse files with broken quoting instead of shredding them
A file with a PRE-EXISTING stray unbalanced '"' -- 1990s mudlib archives
ship many that never compiled -- was silently rewritten into garbage:
past the stray quote the tokenizer's string/code sense inverts, so real
string content lexes as code tokens (every CJK character a separate
'unknown' token the formatter space-separates, '\n' escapes torn into
'\ n') and everything the formatter renders from those tokens shreds.
200+ real files across a 91-mudlib corpus scan were corrupted this way.

Driver-lexer ground truth (src/compiler/internal/lexer.l): hitting EOF
inside a string, template literal, block comment, char literal, or text
block is a hard lexerror ("End of file in string" / "End of file in a
comment" / "End of file in template literal" / lpc_lex_char_error /
heredoc-terminator error). Such a file has NO well-defined token
stream, so per the documented safety contract (docs/lpc/formatter.md)
the only correct behavior is refusal: report, leave byte-identical,
exit nonzero. No speculative "fixed" formatting is invented.

Root cause of the silence: the corpus safety net (token-sequence
equivalence + literal byte-identity + idempotency) re-tokenizes the
output with the same tokenizer, and input and output mis-lex
IDENTICALLY -- the same self-check blind spot as the "(::" (d64c3fa4)
and "'''" (fd3f8b53) fixes; neither of those branches affects this bug
(verified: shredding reproduces with both merged). Unlike those two,
this is not a tokenizer decision that can be corrected -- the input
itself is lexically meaningless past the stray quote -- so the fix is a
new gate rather than a lexing change:

- tokenizer.mjs: any spanning token that reaches end-of-input without
  its terminator (string/char/template/'/*' comment/text block) is
  emitted with `unterminated: true`, mirroring the driver's <<EOF>>
  lexerrors one-for-one. Directive-embedded unterminated quotes stay
  unflagged (line-bounded by design; legal per the driver).
- format.mjs: formatLPC throws up front when any token carries the
  flag ("unterminated string starting at line N ... refusing to
  format"), which format-corpus.mjs already turns into FORMAT ERROR +
  leave-untouched + nonzero exit. This closes the whole class for this
  failure mode: no token comparison is involved, so identical mis-lex
  on both sides can no longer pass.
- bin/format-corpus.mjs: the idempotency re-format of the candidate
  OUTPUT is now inside a try -- a throw there is a per-file refusal,
  not a crash of the whole run (input passing the gate does not prove
  the output would).
- testsuite/format.sh: exclude the three deliberately-unterminated
  EOF-lexerror fail fixtures (eof_in_string / eof_in_comment /
  bad_at_block) alongside the two raw-byte bad-UTF-8 ones -- their
  brokenness is the point and the formatter now refuses them by
  design. --check goes from 779 to 776 files; errors (0) and the one
  pre-existing wouldChange (testsuite/tmp_eval_file.c) are unchanged
  vs master.
- test.mjs: regression tests -- a minimal unbalanced-quote fixture in
  the real-world shape (missing close quote on a key string, CJK + \n
  escape content following) must flag exactly the EOF-swallowing
  string token and make formatLPC throw, its balanced sibling must
  still format; all five EOF-swallowing constructs are refused the
  same way, terminated siblings unflagged. The old "formatter is
  stable on a source that swallows to EOF" check asserted the
  superseded behavior and now asserts the refusal. All three fail
  against the pre-fix code (verified by swapping master's files in).
- docs/lpc/formatter.md, README.md: document the clean-lex gate as
  safety guarantee #1 and the fixture exclusions.

Verified on the three sample corruptions from the corpus scan
(beimeixiakexing2001 qiyuan3.lpc, xinkuangxiangkongjian2
god_weapon.lpc, xiyangzaixian3 vendor_sale.lpc -- 57/387/363 double
quotes, all odd counts): master shreds all three and reports the run
clean; with this fix all three are refused with FORMAT ERROR, left
byte-identical, exit 1.

node tools/lpc-syntax/test.mjs: all pass.
testsuite/format.sh --check: 776 files, 0 errors, 1 pre-existing
unrelated wouldChange (testsuite/tmp_eval_file.c, identical to
master's 779-file baseline modulo the three by-design exclusions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyQCUoTo1Z93Py9aVFHQi1
2026-07-23 20:28:21 -07:00
Yucong Sun
007bb86370 feat: synthetic resolve() on WASM instead of raising an LPC error
The WASM build's dns_stub.cc made resolve() raise "DNS resolver is not
available", which crashes mudlibs that call resolve() during boot/login
(e.g. security daemons whose create() resolves before initializing
state) -- an LPC error there aborts object setup entirely.

There is still no nameserver to consult on this target (the page is the
only peer), so instead of failing, resolve() now succeeds synthetically
while keeping the native resolver's exact contract (dns_libevent.cc):

  - returns an incrementing int key immediately;
  - schedules the callback (string apply or function pointer) on the
    next gametick, never before resolve() returns;
  - callback args are (name, ip, key): ip echoes the input when it is
    already a numeric IPv4/IPv6 address (matching what a native
    getaddrinfo() of a dotted quad yields) and is "127.0.0.1"
    otherwise -- the same loopback every WASM connection reports from
    query_ip_number();
  - this_player() is preserved into the callback when the
    'this_player in call_out' setting is on, and DEBUGMALLOC_EXTENSIONS
    marking of in-flight queries mirrors the native implementation.

LPC callers cannot tell the difference from a native resolve() that
found the host at loopback. Docs (docs/build-wasm.md limits,
src/wasm/README.md) updated.

Native builds compile dns_libevent.cc, not this file: no native change.

Verified under node: resolve("mud.example.com", f) returns key 0 and
later delivers f("mud.example.com", "127.0.0.1", 0); resolve of
"10.20.30.40" echoes the address; full WASM testsuite passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyQCUoTo1Z93Py9aVFHQi1
2026-07-23 20:00:44 -07:00
Yucong Sun
f3e5bfa799 compiler: eliminate C-stack recursion from macro expansion and lexer; raise nesting cap to 65535
The macro-expansion rescan path recursed one yylex() frame per nesting
level, the textual argument pre-expander (lpc_lex_expand_string) recursed
per level with per-level guard-vector copies, the #if/#elif evaluator
recursed per unary/paren/ternary token, and the lexer's no-token recovery
paths (malformed heredocs, over-long $N, template-interpolation close)
retried via recursive yylex() -- error REPORTING stops after 5 parse
errors but scanning does not, so runs of malformed constructs nested a
frame each (a 600KB file of '@' lines segfaulted the driver).

All of these are now iterative:
- lpc_lex_resolve_identifier returns LPC_TOKEN_RESCAN (no token); the
  identifier rule falls through and the SAME yylex() frame keeps
  scanning the pushed expansion buffer -- one Flex buffer per level,
  zero C-stack growth.
- lpc_lex_expand_string is an explicit work-stack machine (one shared
  guard stack + O(1) name-count lookups instead of per-level copies).
- the #if evaluator is an explicit-stack machine (ifexpr_eval), depth
  bounded by token count on the heap; no cap needed. Keeps the audit's
  int64_t retyping (LLP64 correctness).
- heredoc recovery / @@ splice, over-long $N, and template '}' close
  fall through instead of recursing (pinned by lexer_retry_chains.lpc,
  which segfaults the previous binary).

This supersedes the 2026-07-20 audit's stack-overflow mitigations for
the preprocessor (kMaxIfExprDepth, MAX_EXPANSION_NESTING lowered to 32
under the sanitizer build's measured crash boundary): the recursion
itself is gone, so MAX_EXPANSION_NESTING becomes kLpcMaxExpansionNesting
= 65535, shared by both expansion engines as a runaway/memory bound and
counted in LIVE frames only via a live-index stack (dead same-line
provenance frames no longer trip it, so 128+ sequential uses of a macro
on one line compile again). deep_nesting_caps.lpc's deep cases now
compile -- under ASan included -- instead of being rejected; its
comments and AGENTS.md's cap-sizing guidance are updated to the new
design. The self-reference guard is an O(1) hash lookup.
innermost_real_buffer_index -- behind every current_line read, per
matched token -- is O(1) via a maintained include-buffer index stack
instead of walking the whole buffer stack (the walk made deep-chain
compiles quadratic).

Diagnostics keep 16 innermost + 16 outermost expansion notes with an
elision marker instead of one note per level.

Tests: deep_macro_nesting.lpc (60000-deep chains through both engines,
same-line frame accounting, beyond-cap clean error, 262144-token #if
shapes), lexer_retry_chains.lpc, deep_ternary_nesting.lpc (parser-stack
bound pins). Validated post-rebase on Debug and clang ASan+UBSan full
suites (621/621 files each) plus RelWithDebInfo pre-rebase; gtests
320/320.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 01:39:28 -07:00
Yucong Sun
e2e21f4e50
lpc-syntax: move the VS Code extension out to fluffos/fluffos-vscode (#1282)
* docs: point VS Code extension README at the fluffos-vscode packaging repo

Packaged .vsix releases now come from fluffos/fluffos-vscode, which pins
a fluffos commit via submodule and builds this directory from that pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXqX4Kf55ArA39suS9cdCv

* lpc-syntax: move the VS Code extension out to fluffos/fluffos-vscode

The extension source (extension.js, manifest, language configuration)
now lives in the fluffos/fluffos-vscode repo, which pins a fluffos
commit via submodule and syncs this directory's outputs -- the
tokenizer/formatter/linter, lpc-grammar.json, and the TextMate grammar
-- into the packaged extension at build time. tools/lpc-syntax/ keeps
only the grammar-driven engine those builds consume.

- delete tools/lpc-syntax/vscode/ (extension code, generated lib copies)
- generate_ebnf.py: emit_vscode_assets -> emit_tm_language, writing
  lpc.tmLanguage.json at the tools root next to lpc-grammar.json; the
  lib-copy machinery is gone (the other repo syncs the sources directly)
- fix nondeterministic regen: the function-call reserved-word alternation
  sorted a set union with key=len only, so same-length ordering followed
  the per-process string-hash seed; tie-break alphabetically (verified
  identical output under PYTHONHASHSEED=0/1/42)
- test.mjs: retarget tmLanguage checks to the new path; the
  extension-local checks (lib-copy identity, extension.js wiring,
  language-configuration) moved to fluffos-vscode's scripts/test.mjs
- docs: tools/lpc-syntax/README.md, AGENTS.md paragraph 11, docs/lpc/formatter.md

grammar.ebnf and lpc-grammar.json regenerate byte-identical (bison
3.8.2); the moved tmLanguage differs from the old committed copy only in
the same-length tie order inside the function-call lookahead, which is
order-independent for matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXqX4Kf55ArA39suS9cdCv

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:44:50 -07:00
Yucong Sun
ca0119d39a
docs: LPC style guide + formatter reference; for (;;) spacing fix (#1281)
Follow-up to #1270 (the LPC formatter and testsuite reformat).

Adds two hand-authored pages to the LPC Language docs tree, wired into
sidebars.ts and lpc/index.md (zh-CN sidebar keys rescaffolded, new
labels translated):

- docs/lpc/style-guide.md -- the LPC house style. Grounded in the
  Google C++ Style Guide (the base of the driver's own
  src/.clang-format) and MaJerle's community C style guide, adapted
  where LPC differs, with conventions measured from the testsuite
  corpus. Split into the formatter-enforced layout layer (indent,
  100-column limit, K&R braces, the full spacing table, case labels,
  follow-the-source line breaks) and the judgment layer (naming,
  visibility, comments, preprocessor and error-handling practice).
- docs/lpc/formatter.md -- user-facing formatter reference: running it
  (testsuite/format.sh, bin/format-corpus.mjs on arbitrary file sets,
  the formatLPC() API, VS Code settings), options and why their
  defaults mirror src/.clang-format, the normalize-vs-preserve
  contract, the three write gates (token-sequence equivalence, literal
  byte-identity, idempotency), the known unbracketed-continuation
  limitation, and the relation to the C++ style.

Verifying the formatter against every rule the docs state surfaced one
real discrepancy, fixed here: the empty-for-clause spacing rule was
spacing the FULLY-empty header (`for (; ; )`). The pristine corpus and
clang-format both write that form tight; partially-empty headers keep
their spaced empty clauses. `for (;;)` now renders tight while
`for (i = 0; ; i++)` and `for (x = 1; ; )` are unchanged (tight only
when everything back to the opening '(' is semicolons). No corpus file
changes shape (the corpus's only `for(;;)` sits inside a comment).

test.mjs gains a "docs contract" battery: 27 fixed-point spellings
taken verbatim from the style guide plus the normalization cases, so a
formatter change that breaks a documented rule -- or a doc edit that
misstates the formatter -- fails the suite. vscode/lib/format.mjs is
the regenerated copy.

Validated: node tools/lpc-syntax/test.mjs green (172 checks),
testsuite/format.sh --check clean on the current corpus, corpus-wide
token/literal/idempotency and width-sweep harnesses clean, and a full
two-locale Docusaurus build with no broken links.


Claude-Session: https://claude.ai/code/session_01HSL1G3iHXu1dd8XhnBQ2fe

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-17 18:49:03 -07:00
Michael Programs
1cabb728e1
docs: document dual (expr)/{ block } syntax for catch and time_expression (#1278)
Both are compiler keywords sharing the same expr_or_block grammar
production, but the pages showed only the function-call form. Document
the parenthesized-expression and block forms, that the body's value is
discarded, and the compile-time break/continue restriction; cross-
reference the two pages. Update the zh-CN mirrors and port the catch
examples to the Chinese page.
2026-07-17 11:56:22 -07:00
Yucong Sun
86d13cdb83
reference loops: docs, runtime cycle efuns, orphan collector, copy() unwind fix (#1276)
A complete treatment of reference loops (cyclic data structures) in LPC:
the reference-counting VM has no cycle collector, so a value that reaches
itself leaks permanently -- and silently -- once the last outside
reference is dropped, and it cannot be saved, deep-copied, or fully
printed in the meantime. This change documents the problem, fixes a
driver memory-safety bug it exposes, and adds runtime tooling to detect,
locate, break, and (on debug builds) collect such loops.

Driver fixes:

- copy() on a cyclic structure always hits the MAX_SAVE_SVALUE_DEPTH
  error(), and that unwind path leaked every partially-built container --
  allocated with the _empty_ (uninitialized-svalue) allocators and holding
  a borrowed, un-ref-counted pointer to the ORIGINAL value, which the
  Debug memory checker then read after free (heap-use-after-free abort
  under ASan, reproducible with just catch(copy(a)) on a[0] = a).
  deep_copy_* now allocate zero-filled, hold the destination in a
  unique_ptr whose deleter is the tag-symmetric free_*, and only write the
  destination slot after the child copy fully succeeded (ffi.cc precedent,
  AGENTS.md section 4).
- save_object/save_variable and copy()'s 'nested too deep' errors -- the
  classic symptom of a loop -- now say so (the has_cycle() pointer is
  gated on PACKAGE_CONTRIB so core never recommends an efun the build
  lacks).

New efuns (contrib package, src/packages/contrib/cycles.cc):

- has_cycle(mixed): 1 if the value's reference graph contains a loop.
- find_cycles(mixed): one index-path string per loop-closing slot
  ("[3][\"peer\"].1" style).
- break_cycles(mixed): clears every loop in place and returns the number
  of edges broken. Exactly the DFS back-edges are touched (a digraph is
  acyclic iff its DFS has no back-edges): item/value slots are zeroed, a
  loop closed in mapping-KEY position has its node deleted (hashed keys
  cannot be overwritten), and a loop closing on the funptr->args edge
  itself -- possible because bind() SHARES the args array between the old
  and new funptr -- detaches the bound funptr's args list and replaces it
  with a zero-filled one of the same size. DAG sharing is never touched;
  one cut un-loops a whole ring; afterwards the value saves, copies,
  prints, and frees normally.

  All three share one ITERATIVE walk (explicit heap stack, white/grey/
  black coloring): no C-stack recursion, no depth cap -- arbitrarily deep
  acyclic values scan cleanly where save_variable() errors. Edges:
  array/class items, mapping keys AND values, fp->hdr.args; objects are
  deliberately leaves (loops through object variables are the
  destruct()-managed kind: destruct2() zeroes the variable block).
  break_cycles() records fixes during a mutation-free walk and applies
  them in a post-pass that holds a reference on every touched container,
  zeroes slots before deleting nodes (only node deletion can cascade
  frees), and releases the holds last -- order-independent and safe
  against shared/overlapping fixes.

Orphaned-loop collector (develop package, Debug/DEBUGMALLOC_EXTENSIONS):

- find_orphaned_cycles(int collect): finds -- and with any nonzero
  argument reclaims -- data blocks that are unreachable because only a
  reference loop keeps them alive: the case nothing LPC-level can reach
  anymore. Detection is trial deletion (CPython-gc-style), implemented in
  md_scan_orphaned_cycles (checkmemory.cc): count each array/class/
  mapping/funptr's references held by OTHER data blocks; a block whose
  real ref count exceeds that is externally held (object variables, VM
  stack, call_out, any C++-side holder) and seeds liveness, which
  propagates along data edges; the remainder is loop garbage. No root
  enumeration to get wrong -- every legitimate holder shows up as an
  external ref. Collection: hold a ref on every dead block, sever all
  their child slots (releasing strings/objects/buffers/live values
  normally), then release the holds -- each dead block deallocates with
  nothing left to cascade into.
- check_all_blocks() runs the same scan (skippable via new flag bit 2,
  value 4) and reports 'unreachable data block(s) kept alive only by
  reference loop(s)', so the testsuite's per-file check_memory() gate
  turns a dropped cycle into a hard, attributed failure. That immediately
  caught a real pre-existing leak: tests/std/json.lpc's
  test_encode_circular_references() dropped all four of its
  deliberately-cyclic fixtures on every suite run since it was written.

Tests (testsuite/single/tests/):

- operators/reference_loop.lpc pins the driver contract around loops and
  crashes the unfixed Debug/ASan driver (the copy() unwind UAF).
- efuns/has_cycle.lpc, find_cycles.lpc, break_cycles.lpc cover self/
  mutual/ring loops across arrays, mappings (value and key position),
  classes, funptr args (including the bind()-shared-args case, which was
  unbreakable in an earlier revision of this change), DAG-sharing
  preservation, save/copy working again after a break, idempotency, and a
  5000-deep acyclic walk.
- efuns/find_orphaned_cycles.lpc pins baseline-relative detection of 6
  orphans across three dropped loop shapes, idempotent detection, that
  reachable loops are never classified as garbage, and that collection
  reclaims everything while reachable data survives.
- Every cycle-building test has UNCONDITIONAL teardown (body in catch(),
  find_orphaned_cycles(1) regardless, error re-raised) so a mid-test
  regression stays one [ FAILED ] entry instead of cascading the
  harness's LEAK gate into a suite-wide abort (AGENTS.md section 7).

Docs (Docusaurus, sidebar regenerated; full two-locale build verified):

- new concepts page docs/concepts/general/reference_loops.md: why loops
  leak, what each recursive consumer does, the destruct() exception,
  prevention patterns, the runtime tools, and the debug-build collector;
- efun pages for all four new efuns; check_memory.md documents the new
  scan and flag bit.

Validated on Debug+ASan/UBSan (full LPC suite, randomized order, multiple
runs) and RelWithDebInfo (full suite), 313 GTest unit tests, plus an
8-angle adversarially-verified self-review.

Round-2 self-review (4 fresh angles, adversarially verified) additionally:
- break_cycles() post-pass releases its held container references via an
  RAII guard: allocate_array() there can error() (set_config() can shrink
  __MAX_ARRAY_SIZE__ at runtime below a shared args array's size), and the
  old trailing release loop would have leaked every held ref on that
  unwind (AGENTS.md section 4).
- documented the pre-existing map_delete()-class caveat: deleting a
  key-closed loop's node while an outer unlocked foreach-ref variable is
  aimed at it dangles that variable (not specific to this efun; noted in
  code and doc).
- extended orphan-collector coverage from 6 to 10 blocks: class rings
  (TAG_CLASS candidate/sever/free_class paths), mapping pairs closed in
  KEY position (the collector's in-place key-zeroing sever path), and a
  buffer payload riding an orphaned ring (sever must release it or the
  Debug ref gate trips); added a destructed-object-in-walked-value test
  (render_key + leaf handling).
- docs: refs.md and copy.md now link back to the cycle tooling; zh-CN
  sidebar translation keys rescaffolded; AGENTS.md section 7 documents the
  new hard gate and the catch + find_orphaned_cycles(1) teardown pattern.
- re-entrancy audit (foreach/MAP_LOCKED/locked_map_nodes/merge_arg_lists)
  and LPC-test-semantics audit returned no code defects.


Claude-Session: https://claude.ai/code/session_01VaksxbPjc3hjzghoQPUHRo

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-16 00:06:37 -07:00
Yucong Sun
4a9b2ed2d0
docs: rewrite FFI reference around buffer features, add worked libc examples (#1275)
* docs: rewrite FFI reference around buffer features, add worked libc examples

Update docs/driver/ffi-plan.md now that buffers carry the full byte
toolkit (string/int-array promotion, byte lvalues, range assignment,
foreach, concatenation):

- New "The buffer type does the heavy lifting" section mapping each
  buffer language feature to its FFI use.
- The C-string idiom is now the two-line promotion form
  (buffer b = s; b += ({ 0 })) instead of chained string_encode()
  calls; string_encode stays documented for non-UTF-8 encodings, and
  the doc notes that promotion never reaches inside ffi_call's args
  array (the byte boundary stays explicit).
- New "Worked examples -- calling libc" section: scalars (sqrt/pow/abs),
  strings in (strlen, incl. range assignment into an allocation),
  strings out (getenv + ffi_peek(addr, -1) + string_decode with a NULL
  check), out-parameters (frexp), structs (time/localtime via
  ffi_struct_layout), callbacks (qsort with an LPC comparator), and
  foreach over peeked bytes.
- Sync the reference with the implementation: add ffi_address() (was
  missing from the efun listing), correct ffi_free() (zeroes bytes; GC
  reclaims), ffi_peek(-1) (NUL-terminated read capped at the max
  buffer size config), FFI_POINTER args accepting int addresses
  (0 = NULL), pointer returns always being int addresses, the
  FFI_INT/FFI_LONG aliases, ffi_load("") semantics, the valid_ffi
  operation names with a sample master implementation, and the real
  build default (PACKAGE_FFI ships ON; runtime denies without a
  valid_ffi apply; WASM forces it off).

Every snippet is pinned verbatim by a new testsuite file,
testsuite/single/tests/efuns/ffi_doc_examples.lpc (guarded by
__PACKAGE_FFI__ and the ffi_probe availability fixture like the other
ffi tests). Verified: RelWithDebInfo build, ffi glob (21 files) and the
full LPC suite (600 files) pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT

* docs: add a local build quickstart to AGENTS.md

Record the verified Debian/Ubuntu setup so future agents don't
rediscover it one configure failure at a time: the full apt package
list (CI's packages: lines assume a GitHub runner image that
preinstalls cmake/ninja/libicu-dev), the configure/build commands, the
build/src/driver binary path, -DPACKAGE_DB=OFF as the escape hatch
when no MySQL client dev package is available, that libevent is
vendored and GTest optional, and how to run the LPC suite without a
pipe masking the driver's exit status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT

* docs: rename driver/ffi-plan.md to driver/ffi.md

The page stopped being a plan and is the package reference; name it
accordingly (site URL /driver/ffi/, sidebar label "FFI Package").
Update every reference: source comments (ffi.spec, ffi.cc, both
include/ffi.h copies), tools/ffi (README, generate.py's module
docstring and the two comment lines it emits into generated bindings,
with the committed ffi_genmath.lpc fixture updated to match), the
testsuite doc-examples pin, sidebar_meta.json + regenerated
sidebars.generated.json, and the zh-CN sidebar-label key.

Note: the old /driver/ffi-plan/ URL is not redirected (the site has no
client-redirects plugin configured).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT

* ffi: gate ffi_peek through valid_ffi("peek")

ffi_peek(address, nbytes) copies bytes from an arbitrary native
address, which makes it a process-memory disclosure primitive on its
own (and a driver crash on an unmapped address) -- yet it was the one
dangerous efun in the package not gated by valid_ffi, so any object
could call it whenever PACKAGE_FFI was compiled in, even under a
deny-all master. The other ungated efuns operate only on LPC-owned
buffers and handles (ffi_address reveals a buffer's own address, inert
without a peek/call grant) and stay ungated.

f_ffi_peek now calls check_valid_ffi("peek", address) before touching
any memory, exactly like load/symbol/prepare/callback. The testsuite
master denies a -0xDEAD sentinel address so the denial path is
testable without dereferencing anything; ffi_peek.lpc pins the exact
error. Docs updated: valid_ffi(4), ffi_peek(3), and the op list +
sample master in docs/driver/ffi.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT

* stdlib: add /std/ffi_util, FFI-boundary helpers for package_ffi callers

A pure-LPC library packaging the recurring idioms at the C boundary so
binding code stops re-deriving them:

- cstr(s) / cstr_enc(s, enc): NUL-terminated C strings (buffer
  promotion for UTF-8, string_encode for other byte encodings)
- c_string(addr) / c_string_enc(addr, enc): read a returned char*,
  with the NULL -> 0 convention folded in
- c_out(type_code): a zeroed out-parameter block for T*
- c_argv(strings): a NULL-terminated char*[] plus the kept-alive
  per-string buffers (the lifetime footgun a library should own)
- c_field / c_field_set: struct-field access over ffi_struct_layout()
  layouts

Deliberately consumed via `inherit "/std/ffi_util"` rather than the
simul_efun object: c_string() calls ffi_peek(), gated by
valid_ffi("peek", addr, caller), and inheriting keeps the consuming
object as the security principal the master sees. The docs page
(docs/stdlib/ffi_util, "FFI Utilities" in the sidebar) spells this out.

Tests: testsuite/single/tests/std/ffi_util.lpc exercises every helper
against libc (strlen with both encodings, getenv through c_string,
frexp through c_out, strtol's char **endptr through
c_out(FFI_POINTER), and structural checks on c_argv's pointer array).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 22:09:25 -07:00
Claude
3bdbe9676e www: real ws backpressure fix, teardown leak fix, src/www docs, websocket smoke test in CI
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.
2026-07-14 09:53:26 -05:00
Claude
f45461cb97 web terminals on xterm.js; TUI library fixes; emsdk pin; lws output-wedge fix
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
2026-07-14 09:53:26 -05:00
Yucong Sun
c5f3c3bdc7
docs: document call_out handle validity and mid-compile valid_read behavior (#1252)
Follow-up to reviewing PRs #1068 and #932, both of whose bugs are already
fixed on master:

- #1068 (call_out handles below the allocation counter rejected as
  invalid) was fixed by #1220 with a regression test in
  testsuite/single/tests/efuns/remove_call_out.lpc. Document the
  guaranteed semantics in call_out.md / remove_call_out.md: a handle
  stays valid until it fires or is removed, regardless of newer
  call_outs; 0 is never a valid handle.

- #932 (an error thrown from valid_read during #include handling left
  compile_file's reentrancy guard set, wedging all future loads) is
  fixed by the compiler front-end rewrite: compile_file's scope guard
  restores all compiler state and clears the guard on exception
  unwinding. Document in valid_read.md that the apply runs mid-compile
  for #include checks, must not trigger another compile, and that a
  thrown error aborts only that compilation.


Claude-Session: https://claude.ai/code/session_01JRvUnh3x5MYPWPk2MiiUS2

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-12 02:02:56 -04:00
Yucong Sun
814c4fa5b9
vm: string foreach/ref fixes, buffers as byte arrays (foreach, strict bytes, to_buffer), thorough ref tests; #1196 docs follow-up (#1250)
* docs/tests: fix constructs index regression from #1196; add ref page to sidebar; extend & ref tests

- Restore extension-less links and the text_blocks entry in
  docs/lpc/constructs/index.md (the PR was recreated from a pre-Docusaurus
  branch and reintroduced .html links, which fail the docs build under
  onBrokenLinks: 'throw', and dropped text_blocks)
- Add lpc/constructs/ref to the hand-authored sidebar in docs/sidebars.ts
- Drop the stale VitePress 'layout: doc' frontmatter from ref.md
- Extend testsuite/single/tests/operators/ref.lpc: & in parameter
  declarations, ref keyword in foreach, and bitwise &/&= non-regression

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Skf9CfHwAngWorDNDzPKWp

* vm: fix string foreach crash and off-by-one ref reads; support += / -= on string chars

Three related defects around the shared string-codepoint lvalue
(global_lvalue_codepoint), all reproduced on the unfixed binary:

- Nested foreach over strings SEGFAULTED: the iteration cursor lived in
  the shared global, so the inner loop's F_EXIT_FOREACH reset the
  iterator out from under the outer loop (null deref in
  post_index_to_offset). The EGC cursor now lives in each loop's own
  stack slot (the T_NUMBER slot under the loop variable); the ref case
  re-arms the shared codepoint lvalue every iteration, and exit only
  clears the global when it still points at this loop's string slot.

- foreach (int ref c in str) read the WRONG characters: the shared
  index was advanced before the body ran, so every read through the ref
  was off by one ('abc' summed to 197 instead of 294, and the final
  iteration read one past the end). The global index now stays on the
  current character for the whole body. Writes through the ref still go
  to the loop's by-value stack copy and never reach the iterated
  variable -- semantics pinned by tests/operators/foreach.lpc.

- s[i] += n / s[i] -= n threw "Bad Argument 1 to +=()": F_ADD_EQ and
  f_sub_eq handled buffer byte lvalues (T_LVALUE_BYTE) but not string
  codepoint lvalues (T_LVALUE_CODEPOINT), even though ++/--/= worked.
  Both now route through a new codepoint_lvalue_add() helper and produce
  the resulting character as the rvalue.

Regression tests: nested (plain / ref / multi-byte UTF-8) string foreach
and ref-read correctness in tests/operators/foreach.lpc; compound
assignment on string chars (incl. reverse index, rvalue result, and
non-number rhs error) in tests/operators/string_index.lpc. Verified on
the unfixed binary: the nested-foreach test segfaults, the others fail.
Full LPC suite passes 2x on clang ASan/UBSan Debug and 2x on
RelWithDebInfo.

Docs: correct the ref.md note on foreach-over-strings; document the
string-char lvalue rules in AGENTS.md (section 8 + audit checklist 9)
and the ref/& feature in README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Skf9CfHwAngWorDNDzPKWp

* vm: foreach ref over strings shares the s[i] char-lvalue logic; thorough ref tests

Arming a string-char lvalue now goes through one shared helper,
aim_lvalue_codepoint(): it validates the target EGC (out-of-bounds and
multi-codepoint error cleanly, same messages as s[i]) and points the
shared codepoint state at it. Both s[i] lvalues (push_indexed_lvalue)
and foreach ref loop variables use it, and every write consumer already
funnels into assign_lvalue_codepoint() -- so ref loop chars follow
exactly the s[i] rules:

- single-codepoint characters (and EGCs up to 4 bytes, which index as
  their first codepoint) keep working: reads deliver the character,
  assignments through the loop variable succeed
- wider EGCs (flag emoji, ZWJ sequences) now raise the catchable
  "Indexed character is multi-codepoint" error when the ref loop
  reaches them, instead of silently reading as -1; the non-ref form
  still iterates and delivers -1 for such clusters

tests/operators/ref.lpc is now a thorough pass-by-reference suite:
ref/& parameter declarations and call arguments, by-value contrast,
ref forwarding through call chains, call-site refs to array elements /
mapping values / string chars (forward and reverse index, write-back),
foreach ref over arrays / mapping values / strings (read correctness,
multi-byte codepoints, by-value write semantics, assignment error
parity, the multi-codepoint error, non-ref contrast), compile-time
rejections via generated sources (ref outside an argument list, ref to
a range), and bitwise &/&= non-regression -- 39 checks.

Full LPC suite passes 2x on RelWithDebInfo and 2x on a clean clang
ASan/UBSan Debug build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Skf9CfHwAngWorDNDzPKWp

* tests: cover ref across all LPC types

Extend tests/operators/ref.lpc to 58 checks: ref parameters of every
value type (int, float, string, array, mapping, object, function,
buffer, class, mixed) verifying reassignment propagates; the by-value
contrast for reference-typed containers (member writes propagate,
reassignment doesn't); call-site refs to buffer bytes (forward and
reverse index) and class members (both . and -> spellings); foreach
ref over mapping values of mixed types; the compile-time rejection of
a ref mapping KEY in foreach; and the clean runtime error for foreach
over a non-iterable type (buffer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Skf9CfHwAngWorDNDzPKWp

* buffers behave like byte arrays: foreach, strict 0..255 bytes, string/array promotion, to_buffer()

Buffers are now first-class byte containers:

- foreach iterates a buffer like an array, delivering each byte as an
  unsigned int 0..255. A ref loop variable mutates the buffer in place;
  each ref carries its OWN T_LVALUE_BYTE (in ref->sv), so nested buffer
  ref loops and b[i] lvalues in the body can't alias each other.
  T_LVALUE_BYTE consumers now read the lvalue's own pointer/subtype
  instead of reaching for the shared global_lvalue_byte (which remains
  only as the scratch instance b[i] arms).

- every LPC byte write path (=, ++, --, +=, -=) range-checks the result:
  a value outside 0..255 raises "Buffer byte value out of range" and
  leaves the byte unchanged, instead of silently truncating/wrapping.
  += / -= on bytes also yield the resulting value as their rvalue.

- strings and arrays of ints 0..255 PROMOTE to buffers: a new
  to_buffer() efun (registered like to_int/to_float) is wrapped around
  the rhs by do_promotions() / rule_expr_assign for 'buffer b = str',
  'b += str', initializers, and 'b + str'; range assignment and the
  runtime + / += paths convert unpromoted (mixed) values through the
  same svalue_to_buffer_bytes() helper. A string contributes its raw
  UTF-8 bytes; an array must hold only ints 0..255 (validated before
  allocation) or the conversion errors with the target unchanged.

- fixed a pre-existing ref_t leak: a foreach ref loop variable reused by
  a re-entered inner loop leaked one ref per outer iteration (flagged by
  the debug memory checker as 'Found temporary block: make_ref').

tests/operators/buffer_bytes.lpc pins the byte-range semantics, + / +=
concatenation, all promotion forms, and the error paths (106 checks);
foreach.lpc and ref.lpc pin buffer iteration and ref-loop independence;
buffer_range_assign.lpc gains range-read pins. New docs for to_buffer
(sidebar regenerated) and a rewritten lpc/types/buffer page.

Full LPC suite passes 2x on RelWithDebInfo and 2x on a clean clang
ASan/UBSan Debug build (no ref-checker warnings); GTest suite 312/312.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Skf9CfHwAngWorDNDzPKWp

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-12 01:39:30 -04:00
gesslar
1ad5953549
docs: validate "See Also" references, drop dead ones, document valid_ffi (#1251)
* empty

* docs: validate "See Also" references, drop dead ones, document valid_ffi

Reviewed the "See Also" section of every doc and dropped man-page
cross-references whose target page does not exist (verified against the
driver source, not just the docs tree):

  - errorp, each, opcprof, dump_socket_status, extract, shadowp,
    destruct_env_of, move, inventory_visible, inventory_accessible

These name efuns/applies that no longer exist in FluffOS. keys/values now
point at the `for` construct in place of the defunct `each` efun.

valid_ffi was the one "referenced but undocumented" case: it is a real
master apply (APPLY_VALID_FFI) gating every ffi_load/symbol/prepare/
callback, so it gets a proper apply page rather than having its references
removed. Sidebar regenerated to include it.

The 13 modern markdown-link "See Also" sections were already clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 01:09:17 -04:00
gesslar
1ba6584da5
feat: allow & as syntactic sugar for ref (#1196)
Adds '&' as an alternative to the 'ref' keyword for pass-by-reference in
parameter declarations, call arguments, and foreach loops. One-line grammar
change (the 'ref' rule now accepts both L_REF and '&'); no ambiguity with
binary bitwise AND since ref is always in prefix position. Includes
regenerated parser, testsuite coverage, and new docs for the ref construct.
2026-07-11 22:45:47 -04:00
gesslar
6e6d1c772b
docs: flesh out 31 TBW efun reference pages (#1248)
* empty

* docs: flesh out 31 TBW efun reference pages

Replace the placeholder "TBW" DESCRIPTION and type-only SYNOPSIS in 31
efun docs with real descriptions and named parameters, verified against
the driver source:

- math (general/): log10, log2, norm, dotprod, distance, angle
- matrix (general/): id_matrix, translate, scale, rotate_x/y/z,
  lookat_rotate, lookat_rotate2 -- note the in-place mutation of the
  passed matrix and that rotations are in degrees
- compress (general/): compress, uncompress, compress_file,
  uncompress_file -- note the file variants delete the source on success
- interactive/: send_zmp, act_mxp, request_term_type,
  start_request_term_type, request_term_size
- internals/: dump_trace, destructed_objects, check_memory (flag
  bitmask + DEBUGMALLOC build requirement), dump_stralloc, dump_jemalloc
- core: next_bit, explode_reversible, shallow_inherit_list

Each page keeps the existing manpage-style layout (4-space-indented
NAME/SYNOPSIS/DESCRIPTION, name(3) SEE ALSO). Example blocks are indented
to match, so all files are markdownlint-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:07:47 -04:00
Yucong Sun
89060c5a6b
docs: fully-expandable generated sidebar + Chinese docs via Docusaurus i18n (#1246)
* 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>
2026-07-11 17:20:25 -04:00
Yucong Sun
5f7be1008f
Char-mode input: deliver real keystrokes (BS/DEL, whole UTF-8, raw ESC); fix NAWS lost at logon (#1245)
* Char-mode input: deliver real keystrokes (BS/DEL, whole UTF-8, raw ESC); fix NAWS lost at logon

Five input-path fixes that make raw-keystroke (get_char) applications
viable, found by building the LPC TUI library on top of them:

- comm.cc: char mode delivered "" for Backspace/Delete (the byte was
  zeroed before delivery, making BS/DEL/NUL indistinguishable). The
  literal byte is now delivered; line-mode in-buffer editing unchanged.
- comm.cc: char mode delivered one *byte* per callback, splitting a
  multi-byte UTF-8 character into 2-4 invalid one-byte strings.
  Extraction is now UTF-8 aware: a complete sequence arrives as one
  callback carrying one valid character; malformed bytes still go
  byte-at-a-time (no stalls).
- comm.cc: the "no ansi" + "strip before process input" ESC->space
  rewrite (both default on) also applied to char mode, so arrow keys
  arrived as literal "[A". The rewrite is an anti-ANSI-injection
  protection for line-mode commands; char mode now always passes ESC
  through.
- net/telnet.cc: each received chunk was u8_sanitize()d independently,
  so a UTF-8 character split across TCP segments became U+FFFD in any
  input mode. An incomplete trailing sequence (new u8_incomplete_tail(),
  GTest-covered) is now carried over in interactive_t and prepended to
  the next chunk.
- net/telnet.cc + comm.cc: fast clients answer the initial DO NAWS
  while ip->ob is still the master object, so the window_size apply
  fired on the wrong object and the size was lost until the next
  resize. The last report is cached and replayed on the user object at
  logon.

docs/efun/interactive/get_char.md documents the delivery contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE

* Add /std/tui: an LPC TUI library (readline + ncurses for the prompt line and full-screen apps)

A terminal-UI toolkit in pure LPC, strictly layered (design + rationale
in testsuite/std/tui/DESIGN.md; user docs in docs/concepts/general/tui.md):

- ansi.lpc: escape builders plus the width toolkit the driver lacks
  (visible_width/wslice/wpad are ANSI-blind and wide-char aware).
- keys.lpc: keystroke decoder state machine: the get_char byte stream ->
  key events (CSI/SS3 with xterm modifiers, Alt prefixes, bracketed
  paste as one event, SGR mouse, UTF-8, lone-ESC via caller-driven
  flush()).
- readline.lpc: the line editor: emacs keymap (motion/kill/yank/
  transpose), Up/Down history, C-r/C-s incremental search, Tab
  completion, masked mode, horizontal scrolling with wide-char aware
  viewport; repaints only its own line so it works at any scroll
  position.
- screen.lpc: virtual cell grid + minimal-diff frame renderer with
  copy-on-write rows (frame cost tracks touched rows, not W x H);
  wide chars own two cells, boxes/fills/attrs as SGR param strings.
- widget.lpc + w/ (label, list, textfield) + app.lpc: widget protocol,
  focus cycling, event routing; textfield embeds a readline engine.
- terminal.lpc: the one impure module, inherited by the user object:
  get_char re-arm loop (I_NOECHO across re-arms), NAWS/TTYPE caching
  via the window_size/terminal_type applies, lone-ESC walltime timeout,
  guaranteed teardown. tui_readline() is a drop-in input_to()
  replacement; tui_open() runs full-screen apps on the alt screen.

Everything below terminal.lpc is a pure state machine: 194 checks in
single/tests/std/tui/ cover the decoder, editor sessions, history
search, screen diffing and widgets headlessly. `tuidemo` /
`tuidemo app` (command/tuidemo.lpc) demo both modes over any telnet
client; the whole stack was verified end-to-end over a live connection
including split-packet UTF-8, modifier keys, paste, resize and
alternate-screen teardown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE

* /std/tui v2: pterm/blessed-inspired widgets, inline printers and prompts, showcases

Reviewed the pterm (Go) and blessed widget catalogs and ported what earns
its keep in a MUD; DESIGN.md is renamed to README.md and documents the
full set (including what was deliberately not ported).

New layers:
- print.lpc — pterm-style printers that compose with plain write():
  p_table (boxed, width-aware, header rule), p_tree, p_bars, p_spark,
  p_panel, p_bullets, p_header, p_progress, p_info/success/warn/error,
  and p_bigtext via the existing /std/bitmap_font.
- menu.lpc + terminal glue — inline interactive prompts in the normal
  output flow: tui_select(), tui_multiselect() (Space toggles, list
  windows by height, collapses to a "? prompt: answer" line) and
  tui_confirm() (y/n with a default).

New widgets (/std/tui/w/): table (columns + header + selection), tree
(collapsible, arrows fold), checklist, radiolist, button, progress,
spinner (app-driven tick()), and log (bottom-anchored scrollback pane).

Showcases (command/tuidemo.lpc): `tuidemo select` (prompt chain),
`tuidemo print` (all printers), `tuidemo dashboard` (animated spinner,
progress bars, sparkline, live table and log on a call_out tick), and
`tuidemo form` (textfield, radio group, checkboxes, buttons).

Coverage: three new test files (print exact-output, menu sessions,
widgets2) bring /std/tui to 8 files in the suite; the live-connection
e2e run now drives all six showcases end-to-end (52 checks), including
the select->multiselect->confirm chain, unattended dashboard animation,
and full form entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE

* /std/tui: charts — braille canvas, line charts, vertical bars, heatmap

Fills the chart gap left by v2 (only the horizontal p_bars existed):

- canvas.lpc: a braille dot canvas (each cell is a 2x4 dot grid from the
  U+2800 block — the blessed-contrib/drawille technique), giving sub-cell
  resolution: c_set/c_unset/c_get, Bresenham c_line, and c_plot (scale a
  value series across the canvas), with per-cell colour. Renders as a
  string, per-row strings, or per-cell ({ ch, attr }) pairs for blitting
  into a screen.
- print.lpc: p_chart (multi-series braille line chart with y-axis gutter
  and coloured legend), p_vbars (vertical bar chart with eighth-block
  partial tops and optional value row), p_heatmap (2D matrix as
  256-colour cells, cool-to-hot ramp, optional axis labels).
- w/chart.lpc: the live line-chart widget — add_series()/add_point()
  rolling history sized to the widget, auto or fixed y-range; replaces
  the dashboard's sparkline label with a real animating graph.
- `tuidemo charts` showcase; chart docs in README.md and the docs page
  (heatmap removed from the not-ported list).

Tests: single/tests/std/tui/charts.lpc pins the braille bit math with
exact glyphs, line drawing, plot endpoints, exact p_vbars output,
chart/heatmap structure, and the widget's rolling window. The live e2e
run verifies `tuidemo charts` output and braille frames streaming from
the dashboard chart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE

* docs: move the TUI page from Concepts to STDLIB

/std/tui is a mudlib library like base64/break_string/json, so its page
belongs under docs/stdlib/ with the rest of the /std modules, not under
Concepts. Registered in stdlib/index.md; README file-layout pointer
updated. (Concepts' sidebar is autogenerated, so the old entry disappears
with the file.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-11 15:00:36 -04:00
Claude
b0f61c3313 Preserve this_player() in resolve() and async_* callbacks (#1104)
resolve(), async_read(), async_write(), async_getdir() and
async_db_exec() ran their callbacks with no user context, so
this_player() returned 0 and input_to()/printf() were unusable there.

Follow the call_out() precedent: capture command_giver (ref-held) when
the request is registered and restore it around the callback, skipping
destructed objects, gated on the same 'this_player in call_out'
setting. The pending-request holders account for the new ref in the
DEBUGMALLOC walkers.

The new test registers callbacks from two different living objects and
asserts each callback sees its own registrant, which fails without the
per-request capture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv2Gw2AWqtyN3nNEgob1He
2026-07-11 14:56:46 -04:00
Claude
a136a45cd5 Add set_clean_up() efun: schedule an object's next clean_up query (#918)
Mirrors set_reset(): set_clean_up(ob, seconds) records an explicit
one-shot deadline that overrides the idle-time rule in the periodic
sweep; once it fires the object reverts to the idle rule. With the
seconds argument omitted, any pending deadline is cancelled. Both forms
re-flag the object for clean_up consideration (same gate as
request_clean_up(): the object must define a clean_up() apply).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv2Gw2AWqtyN3nNEgob1He
2026-07-11 14:56:46 -04:00
Yucong Sun
6a25f5fe9b
wasm: shrink the driver to ~0.8MB over the wire (33.5MB -> 3.5MB raw) (#1243)
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>
2026-07-11 13:12:44 -04:00
Yucong Sun
d38dc2c833
lexer: strip comments from directive payloads before parsing (#1240) (#1241)
A '//' comment after a #define body was captured INTO the stored macro
body. Expansion buffers carry no newline to end it, so when the macro
expanded inside a spliced line (a function-like macro's substituted
body, as in the report's MIN(credits, m[e][CREDITS])), the '//' ate the
rest of the splice and the parse failed with a baffling 'unexpected ;'
attributed to the outer macro. The same missed strip made
'#undef X // why' erase nothing and '#ifdef X // why' look up the
wrong name and silently take the false branch.

Comments are whitespace (C translation phase 3). dispatch_directive now
strips them from the payload before parsing for #define (name, params,
body -- body also right-trimmed so a stripped comment can't turn '1'
vs '1 ' into a spurious redefinition warning), #undef, #ifdef, #ifndef,
and #pragma (word-list payload); #if/#elif already stripped.
strip_directive_comments() now folds a block comment to ONE space
instead of nothing, so '1 -/*c*/-1' keeps its token boundaries instead
of pasting '--'. #error/#warn/#echo payloads stay raw.

Covered by five new Preprocessor unit tests (the report's repro shape,
paste-prevention via #if branch selection, param-list comment,
testsuite/single/tests/compiler/preprocessor.lpc; documented in
docs/lpc/preprocessor/define.md.

Fixes #1240


Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-11 12:15:19 -04:00
Yucong Sun
82d82f3beb
Add recompile_object() efun: in-place program update, state preserved (#1237)
* Add recompile_object() efun: in-place program update, state preserved

Recompiles a master copy's program from its source file and swaps the
fresh program into the LIVE master copy and every clone sharing it -
the "hot update" alternative to destruct+load_object: nothing is
destructed, so object identity (pointers held elsewhere, name,
inventory, shadows, interactive state, call_outs, heart_beat) is
untouched, and each object's global variables carry over BY NAME
inside the driver (private ones included): the new program's __INIT
runs first, then every surviving name gets its old value back. The
recompile behaves like a normal load - unloaded parents resolve
through the retry dance and the compile-time master applies are
consulted. Returns the number of objects updated.

Made possible by moving an object's variable block OUT of the object_t
allocation into its own (TAG_OBJ_VARS, always >= 1 svalue, wired into
the debug-malloc walkers): every access already went through
ob->variables[i], so a program with a different variable count can now
be swapped onto a live object.

Safety: refused while any object sharing the program is executing
anywhere on the call stack (live frames hold bytecode positions and
variable indices of the old layout), for clones (pass the master
copy), the simul_efun object, pending replace_program(), and nested
calls. Function pointers whose behavior depends on the owner's program
layout (FP_LOCAL, FP_FUNCTIONAL) go stale instead of corrupting:
objects carry a prog_generation stamp, funptrs snapshot it at
creation/bind, and call_function_pointer() errors cleanly on mismatch.

Fixing a latent asymmetry this exposed: make_lfun_funp incremented
func_ref on the creation-time program but dealloc_funp decremented the
owner's CURRENT program. FP_LOCAL pointers now store their program and
account against it symmetrically (checkmemory and %O formatting
updated to match) - caught by the debug-build memory checker in the
testsuite.

The hot-reload daemon's default (state-keeping) path now reloads
through recompile_object() - changed ancestors first, then the watched
program - so clones ride along automatically; a cooperative
hot_reload_state()/hot_reload_restore() pair takes the destruct+load
path with exactly the state it chooses, and watch(prog, 0) opts out
entirely. The daemon test demonstrates finding all live instances with
children()/clonep() and both clone behaviors (updated in place vs.
stragglers on the old program); single/tests/efuns/recompile_object.lpc
pins the efun semantics (master+clones count, per-object state incl.
private, initializers for new variables, removed variables, stale
funptrs, executing/clone/missing-source guards, call_out survival).

Docs: efun reference page, hot-reload guide step 5 rewritten around
the efun with the value-transfer technique kept as the manual
alternative, caveats updated (clone behavior per path, stale funptrs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* recompile_object: support master/simul_efun targets; review fixes

The master object and the simul_efun object can now be recompiled
live. Both subsystems dispatch through cached name->runtime-index
tables (master_applies / simuls) whose entries point into the old
program's function table, so recompile_object() rebuilds them against
the new program immediately after the swap and BEFORE the new
program's __INIT runs (an error inside it would already route through
those tables). Simul_efun indices are preserved by NAME across the
rebuild - that table is deliberately unsorted for exactly this reason
- so simul calls compiled into every other program keep working, and
a simul removed by the new source fails with the usual "no longer a
simul_efun" runtime error. set_master()/set_simul_efun() only
ref/assign when the object actually changes, keeping the classic
destruct-driven replacement path intact. %O of a function pointer to
a since-removed simul now prints a placeholder instead of derefing
the null table entry.

Testsuite: the efun test recompiles the live simul_efun object
mid-run (the very next ASSERT dispatches through the rebuilt table),
pins the currently-executing guard on the master (master::flag() sits
on the call stack for the whole run), and re-runs the master recompile
from a post-run call_out where the master is idle - state carry-over
and apply dispatch are enforced by exiting nonzero.

Also from this self-review round (multi-agent):

* f_recompile_object crashed when the target destructed itself from
  its new program's __INIT: destruct sweeps the VM stack, so the efun
  glue's stack slot held a plain 0 by the time it tried to
  free_object() it. Reproduced by a review agent's probe; the glue
  now uses free_svalue(), and the scenario is pinned in the efun test
  (destructed targets drop out of the updated count).

* hot_reload daemon: ancestors() now returns the inherit closure
  DEEPEST-first - recompiling a middle parent bakes in whatever
  grandparent program is live at that moment, so a >=3-level chain
  with two changed ancestors permanently embedded the stale
  grandparent (reproduced by a review agent; pinned by a new
  kid/mid/grand scenario).

* hot_reload daemon: dep records were map_delete'd before the
  recompile and rebuilt by the applies during it - but a throw BEFORE
  compiling (currently-executing guard, unreadable file) left the
  object loaded with no records, blinding closure_changed() to
  include-file edits forever. Records are now restored when the
  recompile throws (pinned by a new watched-object-drives-the-pass
  scenario).

* docs: inheritance wording ("copies code" -> the child links against
  the exact parent program it was compiled with), the
  currently-executing guard also covers inheritors running inherited
  code, and the cooperative-pair opt-out triggers on
  hot_reload_state() alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* recompile_object: void mid-update replace_program; cover virtuals

Two additions from the C++ review round:

* A replace_program() registered DURING the update slipped past the
  pre-flight check: an earlier target's __INIT can call into a
  not-yet-swapped clone, whose OLD code registers a pending entry -
  computed against the very program the update is replacing. The
  backend sweep then ran that entry's variable-offset shuffle against
  the fresh program's differently-sized variable block (negative
  num_fewer, heap corruption; reproduced under ASan by a review
  agent's probe). recompile_object() now voids any pending entry for
  each target at its swap point - an entry registered AFTER the swap
  is computed against the new program and survives. Pinned in the
  efun test; the rest of the suite run doubles as the sweep detector.

* Virtual objects (materialized through master::compile_object) are
  covered and pinned: the virtual object carries the BACKING file's
  program, so the recompile targets that source and swaps it in with
  the virtual name, identity, flag and state untouched. The testsuite
  master gains a /data/hu/virt* fixture mapping; docs note the
  behavior and that the hot-reload daemon keys its records by
  compiled program name (watch virtuals via their backing file).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* docs: capture hot-reload/recompile_object knowledge in README and AGENTS

README: the hot-reload language bullet now describes what actually
ships (recompile_object with state carried by name, clones included),
and Features gains a Hot Reload section linking the guide.

AGENTS.md, for future agents working on this machinery: the object
variable block is a separate allocation (TAG_OBJ_VARS) and what that
enables; the new-DMALLOC-tag checklist (checkmemory walkers); the
destruct-sweeps-the-VM-stack rule for efun glue; testsuite harness
facts (fixtures outside tests/, unconditional teardown, master::flag
on the stack all run + the post-run call_out pattern, full -ftest
paths, suite side-effect files); the compile-time master applies; and
the recompile_object invariants (executing-frame guard, dispatch-table
rebuild before __INIT, voiding mid-update replace_program entries,
funptr generation staleness, FP_LOCAL func_ref symmetry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* recompile_object: pin shadow/catch_tell/add_action/heart_beat survival

New test recompile_object2.lpc verifies the object-attached runtime
state that dispatches by name keeps working across the swap:
catch_tell routes into the new program while accumulated state stays;
a shadow chain survives updating the SHADOWED object (still
intercepted, new code underneath) and updating the SHADOW itself while
attached; add_action sentences registered by the old code still fire
their verb into the new program; the heart_beat registration persists.

Also two doc wording fixes from the docs review: the executing-guard
bullet now covers both halves of the guard (frames executing the
program's code AND frames belonging to an object of the program
running inherited code), and the guide's mode summary matches the
daemon (hot_reload_state alone selects the cooperative path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* recompile_object: fix simul_efun/__INIT edge cases; cover callback surface

Four defects from the C++ review round, each probe-verified under ASan:

* Recompiling the simul_efun object to a program that defines no simuls
  FREEd the live dispatch table (simul_names/simuls) while other
  compiled programs still carry F_SIMUL_EFUN opcodes and FP_SIMUL
  funptrs with baked indices -> use-after-free on the next simul call.
  Keep the tombstoned arrays instead (remove_simuls() already nulls
  every func, which yields the clean "no longer a simul_efun" error and
  preserves the name->index mapping for re-adds).

* The debug memory checker did not mark IHE_ORPHAN idents as permanent,
  so any run that removed a simul via an update tripped a spurious
  "orphan permanent identifier" leak and failed the testsuite gate.
  Add IHE_ORPHAN to the mark mask (it is part of IHE_PERMANENT).

* The disassembler dereferenced simuls[].func unguarded in two places;
  after a simul removal, dump_prog() on a program referencing it would
  null-deref. Guard both, matching the sprintf %O fix.

* An error() thrown from a target's __INIT during the swap leaked this
  loop's held references (the per-target snapshot ref, new_prog's
  compile ref, the old variable block) and left the update half-applied.
  Wrap call___INIT per target in save_context/try/restore: on error the
  object is left committed to the new program with fresh initializers
  (carried-over state dropped, like a create() that throws during load),
  sibling targets still update, and nothing leaks.

Test coverage:
* recompile_object.lpc: an __INIT that errors -- blueprint and clone
  both recompile, neither is immortalized, the object stays usable on
  the new program, no ref/variable leak (the per-file memory checker is
  the detector). (The simul zero-function / removal paths can't be
  exercised against the shared /single/simul_efun mid-suite; verified
  out-of-band with a throwaway ASan probe that reduces then restores the
  file.)
* recompile_object2.lpc: call_outs armed before the swap fire after it --
  a name-based call_out dispatches into the new program, a funptr
  call_out is stale and is refused cleanly (its target never runs, no
  crash), verified from a post-run call_out. Rounds out the by-name
  callback survivors already covered (catch_tell, add_action, heart_beat,
  shadows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-11 11:24:30 -04:00
Yucong Sun
c364856f0d
lexer: block comments on directive lines may span physical lines (#1236) (#1239)
* tests: clear the active scanner before yylex_destroy in the harnesses

Both tokenizer harnesses destroyed their scanner without
lpc_lex_scanner_destroyed(), leaving the global active_scanner dangling;
the next compile's first current_line read then dereferenced the
destroyed scanner's guts (lpc_lex_current_line_ref ->
innermost_real_buffer_index). Order-dependent and layout-dependent: any
Preprocessor test followed by CompileEntry.FatalInsideIfExpressionRecovers
segfaulted deterministically in a minimal pair and intermittently in full
runs. Pre-existing (reproduces on master); surfaced while adding the
#1236 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe

* lexer: a directive-line block comment may span physical lines (#1236)

The single anchored directive rule captures one physical line (plus
backslash continuations), so a /* comment opened after a #define body
and closed on a later line ended the capture at the newline:
strip_directive_comments() silently swallowed the open comment, the
define itself parsed, and the comment's remaining lines were tokenized
as code -- a regression against the old lexer for a pattern real
mudlibs use.

New lpc_lex_complete_directive() runs before the terminating-newline
consumption: it scans the captured text (quote-aware, same rules as
strip_directive_comments) and, when the line ends inside an open block
comment, pulls raw bytes through lpc_lex_getc() until the comment
closes and the logical line really ends. Comments fold to a single
space, so text after the close still belongs to the directive (C
semantics), and the tail may open further comments, strings, '//', or
backslash continuations. Newlines pulled this way get the same
bookkeeping as the rule's own terminator, and the count is backed out
of lpc_lex_on_directive()'s first-line attribution so diagnostics still
point at the directive. EOF inside the comment reports the same error
as SC_BLOCK_COMMENT's <<EOF>> rule instead of spinning.

Covered by new Preprocessor unit tests (repro shape, comment tail,
__LINE__ bookkeeping, live/dead #if, string-literal '/*', EOF) and
testsuite/single/tests/compiler/preprocessor.lpc pins; documented in
docs/lpc/preprocessor/define.md.

Fixes #1236

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-11 02:07:07 -04:00
Claude
88b21f970b contrib: get_os_env()/set_os_env() with config allow-lists
LPC can now read and set OS environment variables -- the ask from the
dockerization discussion -- gated by two new colon-separated runtime
config allow-lists: 'allowed os environment variables' (readable) and
'writable os environment variables' (writable, implicitly readable).
Both default to empty, so nothing is accessible until the host
administrator opts specific names in. set_os_env(name) with no value
unsets. Windows uses _putenv_s.

Includes efun docs, index registration, regenerated config docs, and a
testsuite case exercising allow/deny/read/write/unset paths (the test
config allow-lists PATH read-only and FLUFFOS_TEST_RW read-write).

Fixes #1045

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
2026-07-11 00:58:09 -04:00
Yucong Sun
e6f0c377f8
wasm: run the full driver in the browser (Emscripten port) (#1231)
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>
2026-07-10 23:33:51 -04:00
Michael Programs
6f33a65c13
build: add pkg-config and libffi to dependency lists (#1235)
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.
2026-07-10 23:26:25 -04:00
Yucong Sun
55956a24b7
Add inherit_program / include_file master applies; auto hot-reload demo (#1230)
* Add inherit_program / include_file master applies; auto hot-reload demo

New compile-time master applies, consulted for every inherit statement
and #include directive:

* mixed inherit_program(string from, string path, int priv)
  Called while compiling `from` for `inherit "path";` (priv nonzero for
  private inherits). A string return is an alternate path for the
  inherited file; an array-of-strings return is the inherited program's
  source itself (compiled via load_object_from_source under the inherit
  statement's name, through the existing load_object retry loop); any
  other return prevents the inheritance.

* mixed include_file(string compiled, string from, string path)
  Called when `from` is about to include `path` while compiling
  `compiled`. A string return is the translated path (resolved absolute
  from the mudlib root or relative to the includer; returning `path`
  unchanged keeps the "..."-vs-<...> search semantics); an
  array-of-strings return is the included text itself (pushed as an
  in-memory include buffer with the usual file-identity bookkeeping);
  any other return prevents the inclusion.

Both follow the valid_override/get_include_path precedent for calling
master LPC mid-compile (skipped without a VM context or master object),
and a missing apply keeps stock behavior.

The applies expose the full compile-time dependency graph, which the
testsuite uses to demonstrate mudlib auto hot-reload on file changes:
/single/hot_reload.lpc registers as the master's compile hooks, records
which source files each program's bytecode was built from (own source,
includes, inherited programs, transitively), and its call_out poller
destructs+reloads watched blueprints whose dependency closure changed
on disk - including reloading a stale parent when only the parent's
include changed.

Testsuite: single/tests/compiler/{inherit_program,include_file}.lpc pin
the apply semantics (redirect, inline source, deny, priv flag, argument
shapes on nested includes) via the scriptable /clone/compile_hook;
single/tests/applies/hot_reload.lpc demonstrates end-to-end hot reload
over a runtime-written inherit+include fixture chain. Docs added for
both applies.

Validated: full LPC suite (532 files) x2 on RelWithDebInfo, x2 on
Debug+ASan/UBSan, plus the 297 GTest unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* docs: add hot reload guide for the new compile-time master applies

New concepts page (concepts/general/hot_reload.md) explaining why
"file changed -> reload" needs the compile-time dependency graph, how
the inherit_program / include_file master applies expose it, and a
step-by-step mudlib implementation with examples: master delegation,
dependency recording, closure computation, change detection with
size+mtime snapshots, parent-first reload ordering, and the call_out
poller. Documents blueprint-reload semantics and caveats (clones keep
the old program, no compiles inside the applies, records only complete
for compiles observed by the daemon), pointing at the testsuite
reference implementation. Cross-linked from both apply reference pages
and the concepts indices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* docs/testsuite: say "master copy", not "blueprint"

Align the hot-reload guide, apply reference, daemon, and test comments
with the project's terminology for the object loaded from a file (see
clonep(3): "the master copy").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* testsuite: edge cases for the compile-time applies; docs review fixes

compile_hooks_edge.lpc pins that unusual inherit_program/include_file
return values produce clean LPC errors or the documented behavior,
never a driver crash: empty array, non-string elements, redirect to
self, empty-string redirect, inline source vs already-loaded object,
multiline inline content, the apply itself throwing mid-compile
(safe_apply falls back to default resolution), extension-spelled
redirects, and denying the auto-included global include file.
Fixtures in /clone/adv_*; /clone/adv_hook wraps the scriptable hook
with a throwing include_file.

Docs fixes from review: the apply-page examples now look the daemon up
with find_object() instead of a path call_other (which would load the
target and trigger a compile mid-compile -- the exact pattern the same
pages forbid), and the hot-reload guide's ancestors() snippet carries
the seen-mapping argument to match the reference daemon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* hot_reload: fix multi-watch and failed-reload handling; harden tests

Three defects from the LPC review round, each now pinned by the
demonstration test:

* check_now() collected and reloaded in one pass, so the first reload's
  snapshot refresh erased the change evidence for every other watched
  program sharing the dependency (a common header, or a watched parent
  iterating before its watched child - which then stayed bound to the
  destructed old parent forever). The stale set is now collected before
  any reload runs.

* A reload whose recompile throws (a syntax error mid-edit - the most
  common event in a hot-reload workflow) unwound poll() before the
  re-arm, killing the poller forever and leaving the watched master
  copy destructed. poll() re-arms first, check_now() catches per
  program, reload_count only counts successes, and closure_changed()
  treats a watched-but-not-loaded program as stale so the retry
  self-heals once the file compiles again.

* The three apply tests registered master compile hooks (and the
  hot-reload test armed the poller and wrote /data/hot) with cleanup on
  the success path only; a thrown check would leave the hook routing
  every remaining compile of the randomized run. Each test now runs its
  checks under catch and unhooks/tears down unconditionally, re-raising
  the error afterward.

The demo test now also covers the shared-dependency pass (watch parent
and child, change the common include, both reload in one pass) and the
broken-edit recovery cycle. The hot-reload guide's snippets are updated
to match the daemon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

* simulate: let inline inherit source resolve its own unloaded inherits

From the C++ review round: master::inherit_program returning inline
source whose text itself inherits a not-yet-loaded program failed the
whole load with "#inherit is not supported when compiling from
in-memory source" -- reachable in the feature's primary use case, since
synthesized programs routinely inherit ordinary on-disk library files.

load_object_from_source() now runs the same iterative dance as
load_object(): when the compile aborts on an unloaded parent, load that
parent (from disk, or from further master-supplied inline source, so
synthesized-inheriting-synthesized chains work), then recompile the
same source string -- which is in hand, unlike the historical
no-filename rationale for rejecting #inherit here. Mirrors
load_object()'s guards: illegal-to-inherit-self, the duplicate-name
check after the parent's arbitrary LPC ran, and an inherit-chain bound
on the retry loop as a backstop against a master that redirects to a
fresh unloaded name on every recompile.

The inherit_program test now covers both new shapes (inline inheriting
unloaded on-disk, and two levels of inline source); the apply doc drops
the already-loaded-only caveat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DGhzJfPhDGJA94EPh1gEK

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 10:27:27 -04:00
Claude
88fe45dd4d review fixes: test hygiene, lpcshell exit scope, dwlib CI coverage, doc nits
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
2026-07-10 10:26:58 -04:00
Claude
ae1baf9b24 docs: register request_clean_up in the efun indexes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
2026-07-10 10:26:58 -04:00
Claude
ea57b78923 dwlib: make the package buildable again (opt-in)
src/packages/dwlib had no CMakeLists.txt, so it silently never built
(for roughly a decade). Add the standard package wiring behind a new
PACKAGE_DWLIB option (default OFF, matching its status as a
Discworld-specific optional package) and fix the one API bit-rot spot
(find_in_mapping now takes the key by value).

Enabling it exposed a build-system gap: packages.fullspec (the make_func
input) did not depend on the generated enabled-package list, so toggling
any package option left a stale efun table -- predefines said the
package existed while its efuns didn't. packages.fullspec now depends on
packages.autogen.h. Also corrects the replace_dollars() test to the
efun's real marker/replacement-pairs API.

Verified: full LPC testsuite passes both with -DPACKAGE_DWLIB=ON (dwlib
efun tests active) and with the default OFF configuration.

Also corrects the clean_up() apply documentation (EN + zh-CN): the
argument is the program's reference count (0 for clones), which the
apply cache can inflate -- not a strict inherited flag (issue #179).

Fixes #467
Fixes #179

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
2026-07-10 10:26:58 -04:00
Claude
75091675af core: member_array() flag 4 runs a function argument as a predicate
member_array(fp, arr, start, 4) now calls fp on each element and
returns the first index where it yields truthy, composing with the
existing prefix (1) and reverse (2) flag bits; without flag 4 a
function argument still matches by pointer identity as before.

Also fixes an adjacent bug: a reverse search (flag 2) that found
nothing returned sizeof(arr) instead of -1, because the found-index
mirroring was applied to the failure sentinel too.

Fixes #900

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
2026-07-10 10:26:58 -04:00
Claude
64eabf810a core: new efun request_clean_up() to resume clean_up queries
Once clean_up() returns 0 the driver clears O_WILL_CLEAN_UP and never
asks that object again, with no way for LPC to opt back in. The new
efun re-arms the flag (for this_object() or a given object), returning
1 on success and 0 when the object defines no clean_up() apply --
mirroring the condition applied at load/clone time.

Fixes #917

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
2026-07-10 10:26:58 -04:00
Claude
6b5e875b54 vm: add 'display preload progress' runtime option
The boot-time preload loop unconditionally printed every file name,
letting errors scroll away between hundreds of progress lines. The new
option (default on, preserving current behavior) silences the per-file
listing when set to 0, leaving errors clearly visible.

Fixes #967

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FMBJLkpkpVpZdz6PWzeWe
2026-07-10 10:26:58 -04:00
Yucong Sun
887f9ebbd1
docs: document UTF-8 native strings in the LPC language reference (#1226)
Add a 'UTF-8 Native Strings' section to lpc/types/strings.md covering
what the driver actually implements: lengths and positions are measured
in extended grapheme clusters (UAX #29), indexing yields code points and
errors on multi-code-point clusters, ranges/explode/strsrch operate on
character boundaries, display width (strwidth, UAX #11) vs length,
\uXXXX and surrogate-pair escapes, UTF-8 validity requirements, and the
encoding boundary (set_encoding for connections, string_encode /
string_decode / buffer_transcode elsewhere). Note in the old
sub-ranging section that positions are characters, not bytes.

Clarify sizeof() (string = grapheme clusters, buffer = bytes) and
strsrch() (character offsets, character-boundary matches).

Every documented example is pinned by a new testsuite file,
testsuite/single/tests/compiler/utf8_doc_examples.lpc, verified against
a freshly built driver (19 checks). Notably replace_string() is
byte-oriented, so it is deliberately NOT listed among the
grapheme-aware operations.


Claude-Session: https://claude.ai/code/session_01TSzcESzU9947zkGzQ6SMmE

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 07:42:20 -04:00
Yucong Sun
c914f03d66
Add local search and documentation guide for docs site (#1221)
* docs: add local full-text search and a contributor README

Add @easyops-cn/docusaurus-search-local to the Docusaurus site so the
docs get an offline search bar (index built at build time, no external
service). English and zh-CN pages are both indexed, and matched terms
are highlighted on the target page.

Add docs/README.md describing the Docusaurus setup, local dev/build
commands, search behavior, directory layout, and gotchas; exclude it
from the published site alongside CLAUDE.md. Point the root README's
docs/ entry at it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSzcESzU9947zkGzQ6SMmE

* docs: remove dead framework leftovers, fix index generation, complete the nav

Delete the VitePress (.vitepress/) and Jekyll (_layouts/, css/) leftovers,
the one-shot migration scripts (fix_md_header.py, fix_seealso.py), and the
stale keywords.json snapshot; prune the matching .gitignore entries and
docusaurus exclude patterns.

Rewrite gen_index.py for Docusaurus: it emitted dead .html links and
legacy 'layout: doc' frontmatter, choked on non-markdown entries, and
dropped nested categories — regenerating an index would have broken it.
It now emits the extension-less links the site actually uses, links
nested category indexes (restoring apply/* on the zh-CN index), and
refuses to run on the docs root. Fix update_index.sh's copy-paste titles
(zh-CN efun/build were titled 'APPLY'), stop it clobbering the
hand-written lpc/index.md, and cover cli/. Regenerated indexes pick up
the missing driver/ffi-plan entry. add_missing_efuns.py now takes the
keywords.json path as an argument instead of requiring a stale copy.

Move CNAME and the Google site-verification file into static/ so they
actually reach the published build output.

Complete the sidebar: link the CLI category to cli/index and add the
missing portbind/symbol/generate_keywords pages, and expose the
previously orphaned stdlib section under Reference.

Promote onBrokenLinks to 'throw' now the build is warning-free, and drop
the empty Demo section from the landing page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSzcESzU9947zkGzQ6SMmE

* docs: strip legacy 'layout: doc' frontmatter from all pages

Mechanical sweep removing the Jekyll-era 'layout: doc' line from every
doc page's frontmatter (Docusaurus ignores it), and the matching line
from the templates in docs/CLAUDE.md so new pages don't reintroduce it.
No content changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSzcESzU9947zkGzQ6SMmE

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-09 22:09:55 -04:00
Yucong Sun
3bd12c657e fix config md 2026-07-09 20:48:48 -04:00
Yucong Sun
816a05cc14 docs/zh-CN: mirror the efun doc pass into the Chinese translation
Applies the English efun review (commit 1e7f9bf5) to the zh-CN tree:

- Translated the 28 newly-documented efuns into Chinese: the 18
  package_ffi efuns and the 10 package_dwlib efuns (add_a, vowel,
  replace, replace_dollars/html/mxp/objects, roulette_wheel,
  query_multiple_short, reference_allowed).
- Return-type fixes where the zh-CN doc still carried the old signature:
  dump_file_descriptors/malloc_status -> string; disable_commands/
  receive/set_heart_beat -> void; get_char/input_to/link -> int. Also
  corrected the stale Chinese return-value prose in disable_commands and
  receive. (cache_stats/mud_status/set_eval_limit were already correct in
  the zh-CN translation.)
- Expanded implode (function-fold form) and save_object (save-to-string
  form).
- Removed the stale opcprof doc and its index / see-also references.
- Regenerated docs/zh-CN/efun/index.md and the efun section of
  docs/zh-CN/index.md.

Docusaurus build passes (onBrokenLinks:throw). New translations omit the
per-file translator-credit line rather than falsely attribute them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00
Yucong Sun
c9b5f2f0eb docs/efun: document FFI + dwlib efuns, fix return-type errors, drop stale docs
Reviewed every efun doc against the .spec source of truth (the make_func
declarations) and the implementations.

New docs for 28 previously-undocumented efuns:
- package_ffi (18): new docs/efun/ffi/ category — ffi_load/unload/symbol/
  prepare/call, alloc/free/sizeof/peek/address/read/write, struct_layout,
  callback/callback_addr/callback_free, error/status.
- package_dwlib (10, under contrib/): add_a, vowel, replace,
  replace_dollars/html/mxp/objects, roulette_wheel, query_multiple_short,
  reference_allowed.

Return-type fixes where the doc contradicted the spec/impl (mostly stale
MudOS behavior): cache_stats/debugmalloc/dump_file_descriptors/
malloc_status/mud_status return string; disable_commands/flush_messages/
receive/set_heart_beat are void; get_char/input_to/link/set_eval_limit
return int; call_stack -> mixed *, function_profile -> mapping *. Also
corrected receive's and disable_commands' stale "returns ..." prose.

Expanded implode (function-fold form) and save_object (save-to-string
form), both of which the docs had omitted.

Removed 4 stale docs for efuns that no longer exist (no spec entry, no
f_ implementation, zero references anywhere): errorp, opcprof, swap,
dump_socket_status.

Regenerated the affected efun index pages. The Docusaurus build passes
with onBrokenLinks:throw, confirming no dangling links.

Note: the docs/zh-CN translated tree still mirrors the old English docs
(same stale entries, and it lacks the new efuns); left for a separate
translation pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00
Yucong Sun
fd705f0634 docs: reconcile FFI plan + preprocessor/diagnostics docs with implementation
Doc-only review pass checking every branch-authored doc against the
shipped code. Fixes where docs described intent rather than the result:

- docs/driver/ffi-plan.md (began life as a plan, drifted from the
  shipped package): ffi_status() returns a mapping, not mixed*;
  callbacks are implemented and in-scope (ffi_callback/_addr/_free added
  to the efun surface and moved out of "v2 deferred"); the DEBUGMALLOC
  section now describes the actual std::unordered_map + TAG_BUFFER scheme
  (no TAG_FFI / mark hook exists); valid_ffi gates load/symbol/prepare/
  callback; testing is the 20-file LPC suite + tools/ffi/test.py (there
  is no GTest fixture).
- docs/lpc/preprocessor/index.md: document the #warn directive.
- docs/lpc/diagnostics.md + preprocessor/pragma.md: show_error_context is
  a legacy flag that no longer changes clang-style compiler diagnostics
  (render_diagnostic ignores PRAGMA_ERROR_CONTEXT; only the runtime
  smart_log path still reads it).
- tools/lpc-syntax/README.md: test.mjs is 49 assertions, not 47.

Verified accurate, no change needed: AGENTS.md, testsuite/README.md,
compiler/internal/README.md, and the source-files / float / strings /
text_blocks / define / conditionals / inherit / include docs, sprintf %g,
and the tools/ffi + vscode READMEs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00
Yucong Sun
9be761aa00 package_ffi: foreign function interface for LPC (libffi), with callbacks
Fully implements the docs/driver/ffi-plan.md design. LPC can now load
native shared libraries, call C functions whose signatures are described
at runtime, manage native memory, pass in/out parameters, and expose LPC
function pointers to C as callbacks.

Package (src/packages/ffi, option PACKAGE_FFI ON, libffi via pkg-config):
- ffi_load/unload/symbol; ffi_prepare/ffi_call (ffi_prep_cif + ffi_call);
  ffi_alloc/free/sizeof/peek/address; ffi_read/write; ffi_struct_layout;
  ffi_callback/ffi_callback_addr/ffi_callback_free (libffi closures that
  re-enter the VM via safe_call_function_pointer); ffi_error/ffi_status.
- Buffers are the currency for all pointer/byte data; raw pointer VALUES
  (returned pointers, buffer/callback addresses) are ints. LPC strings
  are UTF-8-native and never implicitly marshalled -- a char* is a
  buffer the caller encoded (pinned by ffi_string.lpc).
- Native allocations are LPC buffers (GC-tracked); handle tables freed at
  shutdown (ffi_cleanup) and marked for DEBUGMALLOC (mark_ffi).

Security: master apply valid_ffi(op, arg, caller) gates every
load/symbol/prepare/callback (VALID_FFI added to the applies table); a
missing apply denies by default. Optional "ffi allowed libraries" config
allow-list (rc.cc + runtime_config.h + regenerated config.md, new
Security category). __PACKAGE_FFI__ predefine added.

tools/ffi/generate.py: turns a C header into LPC bindings (buffer params
for char*, optional --string-convenience UTF-8 overloads) plus a struct
layout include; reports+skips unsupported forms; --emit-json contract.
Dependency-free test.py.

Tests: 20 testsuite/single/tests/efuns/ffi_*.lpc (every efun, the qsort
callback round trip, the generated-bindings end-to-end path), guarded by
__PACKAGE_FFI__ with a libc-reachability probe. The efuns are VM-stack-
based, so the LPC testsuite is the surface -- libffi's call/closure paths
run there under ASan/UBSan and the per-file check_memory leak gate.

The clang RelWithDebInfo sanitizer caught an error()-unwind leak: both
ffi_prepare and ffi_callback allocated before a code_to_type() that can
error() -- now unique_ptr/custom-deleter owned (AGENTS.md section 4).

Verified: testsuite x3 (ASan Debug) + ctest 297, RelWithDebInfo suite x3
+ ctest 298, clang RelWithDebInfo sanitizer (leak-clean), tools/ffi
test.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00
Yucong Sun
ef4d6744b0 docs/ffi-plan: buffers throughout — no implicit string<->char* marshalling
LPC strings are UTF-8-native, so the FFI boundary must never auto-convert
a string to a C char* (it would impose UTF-8 on APIs expecting other
encodings and cannot carry arbitrary bytes / embedded NULs). Every
pointer and byte payload crosses as a buffer; the LPC caller does the
encoding explicitly with string_encode/string_decode, exactly like the
rest of the driver. Dropped the FFI_STRING type code (a char* is just an
FFI_POINTER buffer); ffi_call args are int|float|buffer only; added
ffi_peek to copy a returned foreign char* into an owned buffer; the
tools/ffi generator emits buffer params for char* (with an opt-in,
clearly-named string-convenience overload). Driver-mediated identifiers
(library path, symbol name) stay string, consistent with the file efuns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00