fluffos/docs/lpc/constructs/inherit.md
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

3.3 KiB

title
constructs / inherit

inherit

The LPC inherit statement

Syntax:

inherit pathname;

where pathname is a full path delimited by quotes (e.g. "/std/Object").

The pathname is resolved like any object name: an extension-less path loads pathname.lpc if it exists and falls back to pathname.c, while an explicit .lpc or .c extension names that exact file (see source files). inherit_list() reports the real file names, extension included.

The master apply inherit_program(from, path, priv) is consulted for every inherit statement before resolution: it can redirect the inheritance to another file, supply the inherited program's source itself (an array-of-strings return), or deny it. Returning path unchanged keeps the behavior described above.

The 'inherit' statement provides the inheritance capability (a concept from object-oriented programming) to LPC objects. Inheritance lets an object inherit functions and variables from other objects. Because the FluffOS driver internally stores global data and compiled code separately, many different objects can use inheritance to share the same piece of compiled code. Each of these objects will have its own local copy of any global variables defined by the object. Suppose that two object A and B inherit object C. Recompiling object either of A or B will not cause C to be recompiled. However, it will cause any global variables provided by C to lose whatever data they had (remember that A and B each have their own copy of the global variables provided by C. Thus updating A will not effect the global variables of B (even those provided by C) and vice versa).

Suppose object A inherits object B. Object A may define variables and functions having the same names as those defined by B. If object A defines a function of the same name as one defined by B, then the definition provided by A overrides the definition provided by B. If A wishes to access the definition provided by B, then it may do so. For example suppose that object A defines its own function named query_long and yet wishes to call the query_long function provided by the /std/Object.c object. Then A may refer to the query_long in Object.c as Object::query_long(). If A defines a variable of the same name as a global variable defined in B, then the only way that A can access that variable is via functions provided by B. If B defines a global variable that is not declared in A, then by default A may use that global variable as if the global variable were defined in A (assuming B does not choose to restrict access). Note: if object B is recompiled, object A will continue to use the old version of object B until object A is also recompiled.

Multiple inheritance is allowed. That is, an object may inherit more than one other object. Suppose special.c inherits weapon.c and armor.c and that both weapon.c and armor.c each provide their own version of query_long(). We may assume that special.c wants to sometimes act like a weapon and sometimes act like armor. When special.c is to look like armor it can use armor::query_long() and when it is to look like a weapon it can use weapon::query_long().

See the tutorial named 'types/modifiers' for more information on how inherited objects may hide data and function definitions from objects that inherit them.