Skip to content

Native backends: hot code reloading by modifying running binary - #31231

Merged
medvednikov merged 27 commits into
vlang:masterfrom
metif12:feat/native-live-reload-v3
Oct 28, 2026
Merged

medvednikov merged 27 commits into
vlang:masterfrom
metif12:feat/native-live-reload-v3

Conversation

@metif12

@metif12 metif12 commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Adds native ARM64 hot reloading on macOS. Calls and function references to @[live] functions
use live pointers; loop checks watch every live source file, rebuild changes, and install new code.
Failed rebuilds publish no partial update, and captured function pointers remain callable after
later reloads. The PR adds examples/hot_reload/native_counter.v and documentation.

-hot-fn <name> extracts a self-contained function as raw machine code. Extraction rejects
relocations and PC-relative references outside the function; calls, literals, or globals needing
address fixups remain unsupported. Linux ARM64 supports ELF object output; executable output is
rejected.

Review fixes:

  • Gate live collection, rewriting, loop checks, and runtime injection on ARM64 capability. Keep
    expression rewriting in a separate helper.
  • Allocate metadata only for live programs. Ordinary programs and forks keep a nil pointer;
    live forks share an immutable snapshot, and recollection creates a fresh snapshot. Keep new
    metadata and preference fields last to preserve existing ordinary field offsets.
  • Correct ELF local/global symbol ordering, relocation remapping, section-relative cstring/data
    offsets, section sizes, and propagated file errors, with regressions for each case.

Validation at a54ff1b284d268a9e0a1b5bca7867045c204d680:

  • A fresh Clang production self-build with ARM64, FastC, and WASM enabled passed. The native test
    passed actual reloads, failed-update retry, captured pointers, methods, multiple source files,
    extraction, and run/test dispatch. Three SSA/WASM files and two extraction/ELF files passed.
  • The documentation check passed: 373 examples, 329 OK, zero warnings, errors, or format errors.
    Formatting and git diff --check passed.
  • A private fixture using an exact transform-module copy passed nil/fork/recollection checks,
    including reads by one pool worker and a forced synchronous caller. This does not establish
    full parallel-dispatch, simultaneous-overlap, or race-detector coverage.
  • A separately matched ordinary production build and all eight focused inspection/output-proof
    stages passed independent review, including ARM64 exclusion and valid byte-identical fixed C.
    Static review retains larger structs and constructor/fork costs; it establishes no universal
    zero-overhead claim.

One matched ordinary self-compilation-to-C comparison against
3e08b99c85a88c98c6d60fdeb547f364fda6edfb completed one warmup per role and ten alternating
AB/BA measured pairs. All 22 runs exited zero with the same 562 files, 559,990 physical source
lines, fixture, standard library, recipe, flags, environment, and output path.

Median metric Baseline Candidate
Retired instructions 48,050,986,319 48,001,171,880
Instructions per parsed line 85,806.864978 85,717.909034
Cycles 23,775,977,993.5 23,710,443,769.5
Elapsed seconds 16.415 14.470
Peak RSS bytes 1,130,250,240 1,130,348,544

Median instructions changed by -0.10366996%; the median paired difference was -91,758,743,
with 2/10 adverse pairs. Independent acceptance covers this fixed ordinary workload only.
Elapsed time and cycles reflect shared host load; no latency gain, universal compiler-efficiency
gain, or enabled-native performance result is established. Raw logs, inventories, compiler hashes,
and output proofs are retained.

Canonical compiler-error, in/out, and full vlib/v/ suites remain pending at this head. The
corrected isolated Linux runner has not launched because host disk space is below its unchanged
16 GiB capacity guard. No canonical suite is currently active or claimed as passing.

Final integration remains pending. If #31161 lands first, the known driver-help conflict must
preserve c|fastc|arm64|x64|wasm|eval, backend-name normalization, and -hot-fn help, followed by
validation of the integrated source. A fresh mergeability check is required before merging.

Pre-merge validation at a54ff1b284d268a9e0a1b5bca7867045c204d680: three consecutive ./v self rebuilds passed, each invoking the preceding output compiler under normal vself policy. The maintainer requested larger suites after this PR batch merges; any earlier pending suite lists above are deferred coverage rather than merge prerequisites.

@metif12
metif12 force-pushed the feat/native-live-reload-v3 branch from 259b420 to f03cd3e Compare October 27, 2026 07:27
@metif12 metif12 changed the title driver, arm64: add -hot-fn raw code extraction for native hot reload Native backends: hot code reloading by modifying running binary Oct 27, 2026
Ports the v2 -hot-fn option (vlib/v2_toberemoved) to v3: stores the
target function name in pref.Preferences.hot_fn and accepts
-hot-fn <name> in the v3 driver. Wiring to ssa.BuildOptions and the
arm64 raw-code extractor follows in later commits.
Ports the v2 hot-fn extract half (vlib/v2_toberemoved) to v3: wires
prefs.hot_fn into ssa.BuildOptions, adds arm64.Gen.extract_function
(slices macho.text_data between symbols), and writes the raw bytes to
-o instead of linking when -hot-fn is set.
Detects @[live] functions during transform, rewrites their calls/references
to __live_<mangled> function pointers, injects live_reload_check() into for
loop bodies, and generates a V runtime file (globals, init, reload loop)
for arm64 builds.
Removes vlib/v/transform/live.v, whose transform_live_fn was never
called; the real injection lives in transform_fn_body/transform_expr
guarded by live_reload_runtime. Adds an experimental native (arm64)
subsection to the hot code reloading docs.
The C backend hot-reload round trip (ORIGINAL->CHANGED->ANOTHER->STOP)
passes on Windows (4/5 local runs; the one failure was a background
process exit-code race, covered by vtest retry: 4). Stop skipping
Windows in testsuite_begin.
Generated __global live_<mangled> is now typed as fn (...) instead of
voidptr, with a type alias per signature, so calls through the pointer
compile. Generated source and initial builds use -enable-globals. Fixes
the unknown-function and global-name errors in the generated runtime.
Adds platform headers for mmap/mprotect/sys_icache_invalidate and fixes
the generated reload command so m/bin are unrolled literals while
live_vexe/live_opts/live_source_file stay runtime consts.
The Windows round trip is flaky in this environment; revert the
default-on Windows enablement and keep FORCE_LIVE_TEST=1 opt-in.
Global live_* pointers and live_reload_check() are generated into the
main module, so only main-module for loops and calls are rewritten.
Imported modules keep their normal call paths.
@metif12
metif12 force-pushed the feat/native-live-reload-v3 branch from 99643f4 to f1e0cdd Compare October 27, 2026 12:26
Compiles examples/hot_reload/message.v with -b arm64 on macOS and
asserts the generated binary links.
Use os.file_last_mod_unix for mtime checks and per-OS page
allocation/protection/flush helpers, so the generated runtime compiles
on macOS, Linux and Windows targets.
Move @[live] collection into collect_live_functions() and call it in
both serial and parallel transform entry points, and share the collected
metadata with forked workers.
Forked transform workers now inherit live_fns, live_methods,
live_source_file and live_reload_runtime so call rewriting also happens
in parallel-transformed function bodies.

@medvednikov medvednikov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the native ARM64 hot-reload implementation for correctness and runtime safety, ignoring CI as requested. I found a source-level build blocker and three issues in the extraction/reload path; details inline. I have not executed the macOS ARM64 reloader on-device.

Comment thread vlib/v/driver/live_reload_source.v Resolved
Comment thread vlib/v/gen/arm64/live.v Resolved
Comment thread vlib/v/driver/live_reload_source.v Resolved
Comment thread vlib/v/driver/live_reload_source.v Resolved
- Remove stray PY token that breaks driver module build.
- Reject functions with relocations in extract_function instead of
  emitting silently broken machine code; add regression test.
- Bound-check executable page writes and rotate pages, keeping
  old pages alive for active function pointers.
- Verify rebuild success and fresh output before swapping pointers;
  only advance live_mtime on full success to allow retry.
- Document self-contained function limitation.
@metif12

metif12 commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Reviewed the native ARM64 hot-reload implementation for correctness and runtime safety, ignoring CI as requested. I found a source-level build blocker and three issues in the extraction/reload path; details inline. I have not executed the macOS ARM64 reloader on-device.

should be fixed

@medvednikov medvednikov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up review of f0730ee (excluding CI): thanks, the four findings from my previous review now have corresponding fixes—the stray PY is gone, relocated object code is rejected, executable page rollover is bounded, and rebuild failures are checked before installing output. However, I found a remaining hole in the relocation check, an allocation-failure path, and a documentation example that the new restriction cannot actually reload. Details inline. I have not executed the reloader on macOS ARM64.

Comment thread vlib/v/gen/arm64/live.v Resolved
Comment thread vlib/v/driver/live_reload_source.v Resolved
Comment thread doc/docs.md Resolved
…example

- extract_function: also decode PC-relative instructions. A call to an
  already-emitted function is a direct BL with baked-in displacement
  and no relocation entry, so it passed the reloc check and copied
  broken. Reject BL/B with targets outside the function and any
  ADRP/ADR; recursion and intra-function branches still accepted.
  Adds regression tests for direct-call rejection and recursion.
- runtime: treat mmap MAP_FAILED like nil via live_page_failed,
  guard the initial mapping and retry allocation on reload, and
  check mprotect results before writes and pointer installs.
- docs: use a self-contained native_counter.v example for the
  native path; label message.v as C shared-library workflow only.
  Add macos-arm64 extraction tests (accept self-contained,
  reject print_message) and remove a dead variable.
Move page allocation/protection/flush helpers out of the common generated
runtime into a target-OS shim file, keeping the common file free of
platform-specific code.

@medvednikov medvednikov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Independent exact-head source reviews and a separate audit approve the final lazy live-metadata implementation. Three consecutive plain ./v self rebuilds passed at this commit under normal vself policy, with a verified output-to-next-input executable chain. Review findings are addressed. The maintainer deferred larger correctness and performance suites until the PR batch merges.

@medvednikov
medvednikov merged commit 3f4b845 into vlang:master Oct 28, 2026
8 of 122 checks passed
@metif12

metif12 commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Follow-up fixes (commit 3a47dc0f60)

All three findings from your second review are addressed:

  1. Direct PC-relative calls -- extract_function now decodes every instruction in the slice. Any BL/B whose target lies outside the function, and any ADRP/ADR, is rejected (these carry no relocation entry and would jump to the wrong address after the copy). Intra-function branches and recursion still pass. Added unit tests test_extract_function_rejects_direct_call_without_reloc and test_extract_function_accepts_recursive_call.

  2. mmap failure -- new live_page_failed treats MAP_FAILED like nil. init() guards the first mapping, live_reload_check() retries allocation on entry, and live_page_protect_rw/rx now return bool and are checked before writes and before installing a function pointer. There is a single guarded install path with exactly one fclose per outcome.

  3. Docs example -- added examples/hot_reload/native_counter.v (a self-contained @[live] function) and switched the native-path docs/examples to it. message.v is now documented as C shared-library workflow only. Added macOS-ARM64-gated tests proving extraction accepts next_value and rejects print_message.

Also removed a dead mutable_cur_module variable in collect_live_functions.

The PR is merged as 3f4b845443; the fixes above are included in that merge. CI was cancelled by you (not a code failure), so no further change is pending from my side. Happy to address anything else.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants