From def49f6159601567bf96b2b8a6d31aed978a28be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 07:43:20 +0200 Subject: [PATCH 1/2] gc: teach gc-root-dominance the referent-with-no-name hole (#7616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checker reported the verbatim pre-#7453 code as clean in every GATED mode — including `--statepoints`, added in #7663 precisely because the other three could not read the lowering that ships. Re-planting #7453's exact code in `expr/url_main.rs` and running every mode over the 16 URL-lowering sources, both lowerings: mode clean sabotaged --moving-only (dominance) 0 0 --unrooted-allocas --moving-only 0 0 --stale-registers --moving-only 2 2 --statepoints --moving-only 2 2 --stale-registers (unfiltered) 24 35 --statepoints (unfiltered) 15 21 Every gated arm blind, both unfiltered arms not: `js_url_coerce_string` is in ALLOC_RE but not in POLL_CAPABLE_RUNTIME, so `--moving-only` drops the window. The one name takes the sabotaged arms to 13 and 8. `--audit-alloc-re` and `--audit-poll-capable` both hunt a NAME WITH NO REFERENT. The new `--audit-poll-reach` hunts a REFERENT WITH NO NAME: a symbol ALLOC_RE matches whose runtime body reaches a POLL_CAPABLE_RUNTIME symbol without being listed. Not "every poll-capable symbol must be listed" (297 call one directly — a coverage change with its own hit count), but "the checker's two lists must not disagree about the same symbol". 77 found, all listed. Shown able to fail: deleting js_url_coerce_string reddens the audit; neutering _strip_noncode reddens the decoy fixture; making the reach one-hop instead of a fixpoint reddens the transitive fixture. Both non-vacuity floors are asserted. Budgets: curated --stale-registers --moving-only 9 -> 13 (pinned 39, unchanged); native --statepoints --moving-only 7 -> 11, re-pinned with all four new hits named. No hit disappeared. --- .github/workflows/gc-root-dominance.yml | 63 +++- changelog.d/7679-poll-reach-audit.md | 82 +++++ docs/src/internals/gc-rooting-invariant.md | 47 ++- scripts/gc_root_dominance_check.py | 393 +++++++++++++++++++++ 4 files changed, 577 insertions(+), 8 deletions(-) create mode 100644 changelog.d/7679-poll-reach-audit.md diff --git a/.github/workflows/gc-root-dominance.yml b/.github/workflows/gc-root-dominance.yml index 663a1b936d..1b7b0e0205 100644 --- a/.github/workflows/gc-root-dominance.yml +++ b/.github/workflows/gc-root-dominance.yml @@ -137,6 +137,36 @@ jobs: - name: POLL_CAPABLE_RUNTIME entries must name real runtime symbols run: python3 scripts/gc_root_dominance_check.py --audit-poll-capable + # ★ The THIRD audit, and the direction the other two structurally cannot + # look (#7616). Both of those hunt a NAME WITH NO REFERENT: a dead + # ALLOC_RE alternative, a phantom POLL_CAPABLE_RUNTIME entry. Nothing + # hunted a REFERENT WITH NO NAME -- a real allocating symbol that is + # missing from POLL_CAPABLE_RUNTIME -- and a missing entry suppresses + # findings exactly as silently as a phantom one. + # + # #7453 is the worked example and it cost a shipped use-after-free. + # `new URL(input, base)` held a raw `*mut StringHeader` across the + # lowering of `base`; the fix added `url_coerce_string` to ALLOC_RE, + # said in its own comment "that gap is why the checker did not flag + # #7453", and stopped one list short. Re-planting that exact code and + # running EVERY mode this workflow has (#7616's measurement): + # + # --moving-only (dominance) 0 clean -> 0 sabotaged + # --unrooted-allocas --moving-only 0 -> 0 + # --stale-registers --moving-only 2 -> 2 + # --statepoints --moving-only 2 -> 2 <- the mode + # that reads the lowering that SHIPS, added in #7663 + # --stale-registers (unfiltered) 24 -> 35 + # --statepoints (unfiltered) 15 -> 21 + # + # Every GATED arm blind, both UNFILTERED arms not. The single missing + # list entry was all of it: adding `js_url_coerce_string` takes the + # sabotaged arms to 13 and 8 and leaves the clean arms at 2 and 2. + # + # Static and instant, like the two above, so it runs before the build. + - name: ALLOC_RE symbols that reach a poll-capable one must be listed + run: python3 scripts/gc_root_dominance_check.py --audit-poll-reach + # `--unrooted-allocas` exempts two heap-value sources whose objects can # neither move nor be reclaimed (#7210): the old-arena class-keys array, # and `js_box_alloc*`'s Box, which is not a GC-heap allocation at all. @@ -331,6 +361,10 @@ jobs: --min-files 90 --min-binds 1500 --min-funcs 1200 \ --max-stale 39 + # 86 -> 104 at #7616: widening POLL_CAPABLE_RUNTIME by 77 symbols makes + # windows MOVING that `--moving-only` previously dropped. Still inside + # the pinned 118, so the budget is deliberately NOT raised — a ratchet + # you loosen every time it gets closer is not a ratchet. - name: Stale-register budget (dependency-scale) run: | set -euo pipefail @@ -395,16 +429,24 @@ jobs: - name: Checker self-test (can this gate still fail?) run: python3 scripts/gc_root_dominance_check.py --self-test - # The three static audits are shared vocabulary, not shadow-mode + # The FOUR static audits are shared vocabulary, not shadow-mode # details: `--statepoints` classifies its sources with the same ALLOC_RE, # decides MOVING with the same POLL_CAPABLE_RUNTIME, and honours the same # #7210 exemptions. A phantom entry in any of them is a hole in THIS gate - # too, so all three run here as well rather than being inherited by + # too, so all four run here as well rather than being inherited by # assumption from a job that may not have run. + # + # `--audit-poll-reach` matters most HERE, because #7616's measurement was + # taken in this job's mode: the re-planted #7453 shape reported 2 clean + # and 2 sabotaged under `--statepoints --moving-only`, i.e. the newest + # mode was as blind as the three it was added to cover for, and for the + # same one-line reason. The full census is in the shadow job above. - name: ALLOC_RE alternatives must match real runtime symbols run: python3 scripts/gc_root_dominance_check.py --audit-alloc-re - name: POLL_CAPABLE_RUNTIME entries must name real runtime symbols run: python3 scripts/gc_root_dominance_check.py --audit-poll-capable + - name: ALLOC_RE symbols that reach a poll-capable one must be listed + run: python3 scripts/gc_root_dominance_check.py --audit-poll-reach - name: Immovable-source exemptions must still hold run: python3 scripts/gc_root_dominance_check.py --audit-immovable-sources @@ -491,6 +533,21 @@ jobs: # 1 capture, a `js_closure_get_capture_bits` read held across # `js_number_coerce`. # + # 11 at #7616, and the four new ones are the POINT of that change + # rather than a regression: widening POLL_CAPABLE_RUNTIME by the 77 + # symbols `--audit-poll-reach` found makes windows MOVING that the + # filter previously dropped. Measured on this corpus, 7 -> 11, with + # no hit disappearing: + # + # 3 test_gap_array_splice_spread::main, `unrooted:alloc`. A fresh + # array held in a raw i64 across `js_array_like_to_array` and + # consumed by `js_array_concat` -- #7453's shape exactly, in the + # spread lowering instead of the URL one. + # 1 test_gap_class_expr_dynamic_parent_ctor, `unrooted:capture`. + # The same `js_closure_get_capture_bits` population as the + # residual above, newly visible because `js_new_function_construct` + # is now classified as a mover. + # # #7664 stays open as this budget's referent: a number with nothing # behind it is the thing CLAUDE.md warns a threshold decays into. # @@ -512,7 +569,7 @@ jobs: --min-statepoints 15000 \ --min-live-bundles 8000 \ --min-relocates 20000 \ - --max-unrooted 7 \ + --max-unrooted 11 \ --max-stale 0 \ --allowlist scripts/gc_root_dominance_allowlist.json \ --seeded-violations 40 \ diff --git a/changelog.d/7679-poll-reach-audit.md b/changelog.d/7679-poll-reach-audit.md new file mode 100644 index 0000000000..985a9cab85 --- /dev/null +++ b/changelog.d/7679-poll-reach-audit.md @@ -0,0 +1,82 @@ +### gc: the root-dominance gate can now see the bug class it was extended for (#7616, #7453) + +`scripts/gc_root_dominance_check.py` reported the verbatim pre-#7453 code as +clean in **every** gated mode, including `--statepoints`, the mode #7663 added +specifically because the other three could not read the lowering that ships. + +**Measured, not argued.** #7453's exact code — a raw `*mut StringHeader` from +`js_url_coerce_string` held across the lowering of `base` and across a second +coercion that allocates — was re-planted in `expr/url_main.rs` and the emitted +IR confirmed to be register-for-register what #7616 quotes +(`%r883 = call i64 @js_url_coerce_string(…)` … `%r886 = call i64 +@js_url_new_with_base(i64 %r883, …)`). Over the 16 `test-files/` sources that +exercise the URL lowerings, both lowerings, both corpora generated by the +production pipeline: + +| mode | clean | sabotaged | +|---|--:|--:| +| `--moving-only` (dominance) | 0 | **0** | +| `--unrooted-allocas --moving-only` | 0 | **0** | +| `--stale-registers --moving-only` | 2 | **2** | +| `--statepoints --moving-only` | 2 | **2** | +| `--stale-registers` (unfiltered) | 24 | 35 | +| `--statepoints` (unfiltered) | 15 | 21 | + +Every **gated** arm blind; both **unfiltered** arms see it. So the new +`--statepoints` mode was as blind as the three it was added to cover for, and +for the same reason: `js_url_coerce_string` is in `ALLOC_RE` but not in +`POLL_CAPABLE_RUNTIME`, and `--moving-only` — which every gated arm runs, in all +four modes — drops any window it cannot classify as MOVING. Adding that one name +takes the sabotaged arms to **13** and **8** and leaves the clean arms at 2 and 2. + +**The generalisation, and why it is not a coverage decision.** `--audit-alloc-re` +catches a dead regex alternative; `--audit-poll-capable` catches an entry naming +no symbol. Both hunt a *name with no referent*. Nothing hunted a *referent with +no name*, and a missing entry suppresses findings exactly as silently as a +phantom one. The new `--audit-poll-reach` asserts something narrower than "every +poll-capable symbol must be listed" (297 exported symbols call one directly — +that is a coverage change with its own hit count): it asserts that **the +checker's two lists must not disagree about the same symbol.** If `ALLOC_RE` +says a call's result is a heap value the checker must track, and the runtime +shows that same call invoking something `POLL_CAPABLE_RUNTIME` already grants can +re-enter JS, the premise for listing it is the premise the set already granted +its callee. + +It found **77** such symbols, `js_url_coerce_string` among them, all now listed. + +**Shown able to fail**, three ways, each verified by planting the fault and +watching the gate go red: + +* deleting `js_url_coerce_string` from `POLL_CAPABLE_RUNTIME` → `--audit-poll-reach` + exits 2 and names it; +* neutering `_strip_noncode` → `--self-test` reports the decoy fixture, whose + only mention of a poll-capable call is inside a `//` comment and a string + literal; +* replacing the reach fixpoint with one hop → `--self-test` reports the + transitive fixture. (This is not hypothetical: the first version of this audit + *was* one-hop, reported 52 names, and found 10 more when re-run after they + were added. An audit that must be run in a loop reports an arbitrary prefix of + its own answer.) + +Both non-vacuity floors are asserted too: an empty symbol scan and a 600-symbol +scan whose bodies are all empty must each be an **error**, not a clean verdict. + +**Budgets — both corpora measured, which is why #7616 was a separate issue.** + +| corpus | arm | before | after | pinned | +|---|---|--:|--:|--:| +| curated (129 sources / 149 modules) | `--moving-only` | 0 | 0 | allowlist, empty | +| curated | `--unrooted-allocas --moving-only` | 0 | 0 | 0 | +| curated | `--stale-registers --moving-only` | 9 | 13 | 39, unchanged | +| native (149 modules, 30632 safepoints) | `--statepoints --moving-only` | 7 | **11** | 7 → **11** | +| dependency-scale (81 zod modules, 67 MB) | `--moving-only` | 0 | 0 | allowlist, empty | +| dependency-scale | `--unrooted-allocas --moving-only` | 0 | 0 | 0 | +| dependency-scale | `--stale-registers --moving-only` | 86 | 104 | 118, unchanged | + +Only `--max-unrooted` moves, and the four new hits are named one by one in the +workflow rather than absorbed into a number: 3 in `test_gap_array_splice_spread` +(a fresh array held in a raw i64 across `js_array_like_to_array` and consumed by +`js_array_concat` — #7453's shape in the spread lowering) and 1 more of the +`js_closure_get_capture_bits` population, newly visible because +`js_new_function_construct` is now classified as a mover. No hit disappeared; +the four new ones are the point of the change, not a regression. diff --git a/docs/src/internals/gc-rooting-invariant.md b/docs/src/internals/gc-rooting-invariant.md index 73dfcbf1e7..77a40585eb 100644 --- a/docs/src/internals/gc-rooting-invariant.md +++ b/docs/src/internals/gc-rooting-invariant.md @@ -275,11 +275,48 @@ like coverage while doing it. Two rounds of this have now been measured: entry points are `js_closure_callN`, which `RECEIVER_SINKS` in the same file already spelled correctly. -`--audit-poll-capable` is the gate for this, and `gc-root-dominance.yml` runs it -alongside `--audit-alloc-re` before the build. It fails on any entry that names -no exported `extern "C" fn js_*`. When it goes red, **replace** the phantom with -the symbol codegen actually emits rather than deleting it — deleting turns the -audit green and leaves the hole. +3. **A real symbol that was simply not in the set** (#7616 / #7453). The two + rounds above are both a NAME WITH NO REFERENT, and both audits look only in + that direction. `new URL(input, base)` held a raw `*mut StringHeader` from + `js_url_coerce_string` across the lowering of `base`; #7453's fix added + `url_coerce_string` to `ALLOC_RE` — its comment says *"that gap is why the + checker did not flag #7453"* — and stopped one list short, so the shape was + never catchable under the mode CI runs. Re-planting that exact code and + running every mode (#7616): + + | mode | clean | sabotaged | + |---|--:|--:| + | `--moving-only` (dominance) | 0 | **0** | + | `--unrooted-allocas --moving-only` | 0 | **0** | + | `--stale-registers --moving-only` | 2 | **2** | + | `--statepoints --moving-only` (the lowering that ships) | 2 | **2** | + | `--stale-registers` (unfiltered) | 24 | 35 | + | `--statepoints` (unfiltered) | 15 | 21 | + + Every gated arm blind, both unfiltered arms not — *including* `--statepoints`, + added in #7663 precisely because the other three were blind to the shipping + lowering. Adding the one name takes the sabotaged arms to 13 and 8 and leaves + the clean arms at 2 and 2. + +`--audit-poll-capable` is the gate for rounds 1–2 and `--audit-poll-reach` is +the gate for round 3; `gc-root-dominance.yml` runs both alongside +`--audit-alloc-re` before the build. `--audit-poll-capable` fails on any entry +that names no exported `extern "C" fn js_*`. When it goes red, **replace** the +phantom with the symbol codegen actually emits rather than deleting it — +deleting turns the audit green and leaves the hole. + +`--audit-poll-reach` fails when a symbol `ALLOC_RE` matches reaches a +`POLL_CAPABLE_RUNTIME` symbol through the runtime's own call graph without being +listed itself. It is deliberately NOT "every poll-capable symbol must be +listed" — 297 exported symbols call one directly, and deciding that is a +coverage change with its own hit count. It asserts only that **the checker's two +lists must not disagree about the same symbol**: if ALLOC_RE says a call's +result is a heap value to track, and the runtime shows that call invoking +something this set already grants can re-enter JS, the premise for listing it is +one the set already granted. The reach relation is a fixpoint over exported +symbols (a one-hop version reported 52 names, and re-running after adding them +found 10 more), and comments and string literals are stripped first so a name +mentioned in prose cannot become a premise. Checking a *plausible* name is not enough. Confirm against emitted IR: diff --git a/scripts/gc_root_dominance_check.py b/scripts/gc_root_dominance_check.py index be62f77737..c5d8c7dc23 100755 --- a/scripts/gc_root_dominance_check.py +++ b/scripts/gc_root_dominance_check.py @@ -778,6 +778,231 @@ def audit_poll_capable(roots=SYMBOL_ROOTS): print("=== every entry names an exported runtime symbol") return 0 + +# ------------------------------------------------------- AUDIT: the third one +# +# `--audit-alloc-re` catches an ALLOC_RE alternative that matches nothing. +# `--audit-poll-capable` catches a POLL_CAPABLE_RUNTIME entry that names +# nothing. Both look for a name with no referent. NEITHER looks for a referent +# with no name, and that is the direction #7616 came from. +# +# ## The shape, measured +# +# #7453: `new URL(input, base)` held a raw `*mut StringHeader` from +# `js_url_coerce_string` across the lowering of `base` and across a second +# coercion that allocates. #7453's own fix added `url_coerce_string` to +# `ALLOC_RE` -- the comment there says in as many words *"That gap is why the +# checker did not flag #7453"* -- and stopped one list short. Re-planting that +# exact code and running every mode the gate has (#7616): +# +# mode clean sabotaged +# --moving-only (dominance) 0 0 +# --unrooted-allocas --moving-only 0 0 +# --stale-registers --moving-only 2 2 +# --statepoints --moving-only (the NATIVE 2 2 +# lowering, #7663's mode, the one that ships) +# --stale-registers (no --moving-only) 24 35 +# --statepoints (no --moving-only) 15 21 +# +# Every GATED arm is blind and both UNFILTERED arms see it. `ALLOC_RE` decides +# whether a register HAS a heap-value source; `POLL_CAPABLE_RUNTIME` decides +# whether the window around it is MOVING, and `--moving-only` -- which is what +# all four gated arms run -- drops everything the second list cannot classify. +# Adding this ONE name takes the sabotaged arm to 13 and 8 respectively, and +# leaves the clean arm at 2 and 2. The bug was catchable by a one-line list +# entry for the entire time the gate was believed to cover it. +# +# ## Why this is an inconsistency and not a coverage decision +# +# The audit below does NOT assert "every poll-capable runtime symbol must be +# listed". That is a coverage question with its own hit count (297 exported +# symbols call a POLL_CAPABLE_RUNTIME symbol directly), and deciding it belongs +# in a change that can measure the new reports -- the same reasoning ALLOC_RE's +# deleted `bigint_\w+_op` alternative records. +# +# It asserts something narrower and purely internal: **the checker's two lists +# must not disagree about the same symbol.** If ALLOC_RE says a call's result +# is a heap value the checker must track, and the runtime shows that same call +# invoking something POLL_CAPABLE_RUNTIME already grants can re-enter JS or run +# a moving minor, then the checker knows both halves and refuses to put them +# together. There is no judgement to make: the premise for listing it is the +# premise it already granted the callee. +# +# ## One-sided in the safe direction, twice +# +# * The reach relation is DIRECT calls only, not a fixpoint through the +# runtime's internal Rust functions. That under-approximates -- a wrapper two +# levels deep is missed -- so the audit can fail to report, never falsely +# report. Widening it to a fixpoint needs a Rust call graph, which is a +# different tool. +# * Line comments and string literals are stripped before the scan, because a +# premise extracted from a comment is a phantom in the same way a +# POLL_CAPABLE_RUNTIME entry naming nothing is. Measured: stripping removed +# exactly one false hit from the first run of this audit. +# +# And the remedy is safe in the checker's own stated direction: "a name that is +# in fact not an allocation costs a false positive to triage, while a missing +# one costs a shipped use-after-free". + +_RUNTIME_CALL_RE = re.compile(r"\b(js_\w+)\s*\(") +_LINE_COMMENT_RE = re.compile(r"//[^\n]*") +_STRING_LIT_RE = re.compile(r'"(?:[^"\\]|\\.)*"') + + +def _strip_noncode(text): + """Line comments and string literals removed, so a name mentioned in prose + cannot become a premise. Block comments are left alone deliberately: they + nest, a regex cannot match them, and a half-correct stripper that ate the + wrong span would drop real code.""" + return _STRING_LIT_RE.sub('""', _LINE_COMMENT_RE.sub("", text)) + + +def _balanced_body(src, start): + """The `{...}` block beginning at or after `start`, or None.""" + open_at = src.find("{", start) + if open_at < 0: + return None + depth = 0 + i = open_at + while i < len(src): + c = src[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + return src[open_at:i + 1] + i += 1 + return None + + +def runtime_symbol_bodies(roots=SYMBOL_ROOTS): + """`js_* -> [body]` for every `extern "C" fn js_*` the runtime exports. + + A symbol can appear more than once (per-platform `cfg` variants), so the + value is a list and every body is scanned. + """ + bodies = defaultdict(list) + for root in roots: + if not os.path.isdir(root): + continue + for dirpath, _dirs, files in os.walk(root): + for name in files: + if not name.endswith(".rs"): + continue + with open(os.path.join(dirpath, name), + encoding="utf-8", errors="replace") as fh: + src = fh.read() + for m in _EXTERN_C_FN_RE.finditer(src): + body = _balanced_body(src, m.end()) + if body is not None: + bodies[m.group(1)].append(_strip_noncode(body)) + return bodies + + +def runtime_direct_callees(bodies): + """`js_* -> {js_* it calls directly}`, self-edges dropped.""" + out = {} + for sym, texts in bodies.items(): + called = set() + for text in texts: + called.update(_RUNTIME_CALL_RE.findall(text)) + called.discard(sym) + out[sym] = called + return out + + +def poll_reaching_runtime_symbols(callees, poll): + """Exported symbols that reach a poll-capable one, transitively. + + A FIXPOINT, not one hop, and the difference is not academic: the first run + of this audit was one-hop, its 52 names were added, and re-running found 10 + MORE that reached through one of the 52. An audit that has to be run in a + loop until it stops finding things is an audit that reports an arbitrary + prefix of its own answer. + """ + reaching = {s for s in callees if callees[s] & poll} + changed = True + while changed: + changed = False + for sym, called in callees.items(): + if sym in reaching or sym in poll: + continue + if called & (reaching | poll): + reaching.add(sym) + changed = True + return reaching + + +def poll_reach_gaps(bodies, poll_capable=None): + """`[(sym, [callees on the path])]` for every ALLOC_RE symbol that reaches + a poll-capable one and is not itself listed. Sorted, so the report and the + remedy are stable across runs.""" + poll = POLL_CAPABLE_RUNTIME if poll_capable is None else poll_capable + callees = runtime_direct_callees(bodies) + reaching = poll_reaching_runtime_symbols(callees, poll) + gaps = [] + for sym in sorted(callees): + if sym in poll or not ALLOC_RE.match(sym) or sym not in reaching: + continue + # Name the direct edge if there is one, otherwise the intermediate the + # reach goes through. The remedy is the same either way, but a reader + # checking the premise needs somewhere to start. + hit = sorted(callees[sym] & poll) or sorted(callees[sym] & reaching) + gaps.append((sym, hit)) + return gaps + + +def audit_poll_reach(roots=SYMBOL_ROOTS, bodies=None): + """Exit status for `--audit-poll-reach`. 0 clean, 2 on a disagreement. + + `bodies` is an injection point for `--self-test`, which has to be able to + drive both non-vacuity floors without a runtime checkout that satisfies + neither. + """ + if bodies is None: + bodies = runtime_symbol_bodies(roots) + # Non-vacuity, for the same reason the other two audits do it -- and here + # it needs TWO floors, because this audit has two ways to measure its own + # scan instead of the lists. Too few symbols means the walk missed the + # crates; too few call EDGES means the body extractor returned empty or + # truncated bodies, which would report a serene zero over nothing. + if len(bodies) < 500: + print(f"error: found only {len(bodies)} `extern \"C\" fn js_*` bodies " + f"under {', '.join(roots)}. The audit is measuring its own scan, " + "not the lists. Run it from the repository root.", file=sys.stderr) + return 2 + callees = runtime_direct_callees(bodies) + exported = set(bodies) + with_edges = sum(1 for s in callees if callees[s] & exported) + if with_edges < 100: + print(f"error: only {with_edges} exported symbols were seen calling " + "another exported one. `_balanced_body` is returning empty or " + "truncated bodies, so this audit would report clean having " + "inspected nothing.", file=sys.stderr) + return 2 + gaps = poll_reach_gaps(bodies) + print(f"=== poll reach: {len(bodies)} exported symbols, {with_edges} with " + f"an intra-runtime call edge, " + f"{sum(1 for s in bodies if ALLOC_RE.match(s))} matched by ALLOC_RE") + if gaps: + print("error: ALLOC_RE symbols that CALL a POLL_CAPABLE_RUNTIME symbol " + "but are not in POLL_CAPABLE_RUNTIME:", file=sys.stderr) + for sym, hit in gaps: + print(f" {sym:<48s} -> {', '.join(hit[:3])}", file=sys.stderr) + print("The checker knows this call's result is a heap value it must " + "track (ALLOC_RE) and knows the thing it calls can re-enter JS " + "or run a moving minor (POLL_CAPABLE_RUNTIME), and refuses to " + "put the two together: a window whose only collection point is " + "one of these classifies MOVING: no, so every `--moving-only` " + "arm -- which is every gated arm, in all four modes -- drops it. " + "That is #7616 exactly. Add the symbol to POLL_CAPABLE_RUNTIME.", + file=sys.stderr) + return 2 + print("=== no ALLOC_RE symbol reaches a poll-capable one unlisted") + return 0 + + # Bit-level / identity producers a heap address flows through unchanged. TRANSPARENT_OPS = ("or i64", "and i64", "bitcast", "inttoptr", "ptrtoint", "select", "phi", "add i64", "sub i64") @@ -983,6 +1208,86 @@ def is_collecting(callee): "js_jsvalue_to_string_method", "js_jsvalue_to_string_radix", "js_string_coerce", "js_string_coerce_method_this", "js_number_coerce", "js_object_coerce", + # ---------------------------------------------------------------- #7616 + # + # The 52 symbols `--audit-poll-reach` found, and the reason they are one + # block rather than filed among the families above: they were not found by + # reading, they were found by asking the checker's own two lists whether + # they agreed about the same symbol. Each one is matched by ALLOC_RE — the + # checker already treats its result as a heap value it must track — and + # each one's runtime body CALLS something already in this set. The premise + # for listing it is the premise this set already granted its callee, so + # there is no per-name judgement to record beyond the callee named in the + # audit's output. + # + # `js_url_coerce_string` is the one #7616 measured: re-planting #7453's + # code made every gated arm report 0 without it and 13 / 8 with it. + "js_arguments_object_alloc", + "js_array_clone", "js_array_flatMap", + "js_array_to_sorted_default", "js_array_to_sorted_with_comparator", + "js_bigint_from_f64", + "js_boxed_number_new", "js_boxed_string_new", + "js_broadcast_channel_new", + "js_create_namespace", + "js_ethers_wallet_create_random", + "js_event_target_new", + "js_iterator_to_array", + "js_new_function_construct", "js_new_function_construct_with_new_target", + "js_node_stream_duplex_new", "js_node_stream_readable_new", + "js_node_stream_writable_new", + "js_object_assign_one", "js_object_assign_validate_target", + "js_object_entries", "js_object_values", + "js_object_from_entries", "js_object_group_by", + "js_object_get_own_property_descriptor", + "js_object_get_own_property_descriptors", + "js_promise_new_with_executor", + "js_proxy_construct", "js_proxy_revocable", + "js_ratelimit_new_from_options", + "js_regexp_construct", + "js_request_new_from_init", + "js_string_concat_chain", "js_string_concat_value", + "js_string_normalize", "js_string_pad_fill", "js_string_repeat", + # The `_dyn` half of the replace family. The block comment above deferred + # these as "a separate coverage decision with their own hit count"; the + # audit answers it — each one calls `js_string_coerce`, which is already + # here, so the deferral was about a premise this set had already granted. + "js_string_replace_regex_dyn", "js_string_replace_string_dyn", + "js_string_replace_search_dyn", + "js_string_replace_all_regex_dyn", "js_string_replace_all_string_dyn", + "js_string_replace_all_search_dyn", + "js_suppressed_error_new", + "js_symbol_new", + "js_text_decoder_new", + "js_url_coerce_string", + "js_value_to_string_with_encoding", + "js_value_to_string_with_encoding_or_radix", + "js_writable_stream_new_from_sink_object", + "js_writable_stream_new_with_sink_type", + "js_ws_server_new", + # The second wave, and the reason `poll_reaching_runtime_symbols` is a + # fixpoint rather than one hop: every one of these reaches a poll-capable + # operation THROUGH one of the names above, or through a shared helper + # (`js_array_length` on a Proxy runs the trap; `js_string_index_to_i32` + # coerces). A one-hop audit reported the block above, went green, and left + # these — an audit that must be re-run in a loop reports an arbitrary + # prefix of its own answer. + "js_array_from_arraylike_holey_value", "js_array_from_async", + "js_array_from_value", "js_array_like_to_array", + "js_dom_exception_new", + "js_map_from_iterable", "js_set_from_iterable", + "js_new_function_construct_apply", + "js_node_sqlite_database_sync_new", + "js_node_stream_passthrough_new", "js_node_stream_transform_new", + "js_object_create_with_props", + "js_object_entries_value", "js_object_values_value", + "js_object_get_own_property_names", "js_object_keys", + "js_object_keys_value", + "js_reflect_construct", "js_regexp_construct_call", + "js_string_from_char_code_array", + "js_string_index_get_boxed", "js_string_substr", + "js_super_construct_apply", + "js_vm_synthetic_module_new", + "js_writable_stream_new", } @@ -4741,10 +5046,88 @@ def _write_allowlist(entries): if not statepoint_self_test(): ok = False + if not poll_reach_self_test(): + ok = False + print("self-test OK" if ok else "self-test FAILED") return 0 if ok else 1 +# The `--audit-poll-reach` half of `--self-test`. Planted, not merely +# exercised: the audit is green on the real runtime today, so a run over the +# real runtime proves only that nothing is currently wrong — the same vacuous +# green the whole #7616 family is about. These fixtures make it report. +_POLL_REACH_FIXTURE = { + # direct: an ALLOC_RE symbol calling a poll-capable one. + "js_widget_new": ["{ let x = js_object_get_field_by_name(o, k); x }"], + # transitive: reaches it only through the direct one above. + "js_widget_new_from_value": ["{ js_widget_new(v) }"], + # two hops through a NON-ALLOC_RE intermediate, which is the shape a + # one-hop audit misses entirely. + "js_gadget_create": ["{ js_helper_that_coerces(v) }"], + "js_helper_that_coerces": ["{ js_string_coerce(v) }"], + # a name that appears ONLY in a comment and a string literal. Reported + # would mean `_strip_noncode` has stopped working and the audit is + # extracting premises from prose. + # Both spellings carry a full CALL, parentheses and all, because + # `_RUNTIME_CALL_RE` needs the paren -- a decoy that only names the symbol + # is caught by the regex rather than by the stripper, and the arm would + # pass with `_strip_noncode` deleted (measured: it did). + "js_decoy_new": ['{ // was js_string_coerce(v) before #0000\n' + ' panic!("js_string_coerce(v) is not called here") }'], + # not matched by ALLOC_RE, so out of scope however poll-capable it is. + "js_something_unrelated": ["{ js_string_coerce(v) }"], +} + + +def poll_reach_self_test(): + ok = True + poll = {"js_object_get_field_by_name", "js_string_coerce"} + # Through the SAME stripper `runtime_symbol_bodies` applies, so the decoy + # arm below tests the real pipeline rather than a hand-cleaned copy of it. + fixture = {sym: [_strip_noncode(b) for b in bodies] + for sym, bodies in _POLL_REACH_FIXTURE.items()} + gaps = dict(poll_reach_gaps(fixture, poll)) + want = {"js_widget_new", "js_widget_new_from_value", "js_gadget_create"} + if set(gaps) != want: + print("self-test FAIL: --audit-poll-reach over the planted fixture -> " + f"{sorted(gaps)}, expected {sorted(want)}", file=sys.stderr) + ok = False + # The control: listing them clears the report, so the audit is answering + # the question it claims and not just enumerating ALLOC_RE. + cleared = poll_reach_gaps(fixture, poll | want) + if cleared: + print("self-test FAIL: adding the planted names to POLL_CAPABLE_RUNTIME " + f"must clear the report; still reports {[g[0] for g in cleared]}", + file=sys.stderr) + ok = False + # Non-vacuity: a scan that found nothing must be an ERROR, not a clean + # verdict. Both floors, because they fail for different reasons. + # stderr is swallowed for the two floor probes: they are SUPPOSED to print + # their error, and letting it through would make a passing self-test look + # like a failing one. + with contextlib.redirect_stderr(io.StringIO()): + empty_scan_rc = audit_poll_reach(roots=("no-such-directory",)) + if empty_scan_rc != 2: + print("self-test FAIL: --audit-poll-reach over an empty symbol scan " + "must be an error, not a pass", file=sys.stderr) + ok = False + # `_balanced_body` returning nothing is the OTHER way this audit can + # measure itself: plenty of symbols are found, every body is empty, and the + # verdict is a serene zero over zero call edges. That is the floor the + # second guard exists for, and it needs its own fixture because the first + # guard would already be satisfied. + empty_bodies = {f"js_symbol_{i}": ["{}"] for i in range(600)} + with contextlib.redirect_stderr(io.StringIO()): + empty_bodies_rc = audit_poll_reach(bodies=empty_bodies) + if empty_bodies_rc != 2: + print("self-test FAIL: --audit-poll-reach over 600 symbols with EMPTY " + "bodies must be an error -- zero call edges means the body " + "extractor is broken, not that the lists agree", file=sys.stderr) + ok = False + return ok + + def main(): ap = argparse.ArgumentParser( description=__doc__.splitlines()[0], @@ -4812,6 +5195,14 @@ def main(): "so a phantom entry is a hole the gate cannot fail " "through -- ten of twenty-eight were phantoms when " "this was added. Takes no corpus.") + ap.add_argument("--audit-poll-reach", action="store_true", + help="the third audit, and the one the other two cannot " + "do: fail on a symbol ALLOC_RE matches whose runtime " + "body CALLS a POLL_CAPABLE_RUNTIME symbol while not " + "being in that set itself. The other two look for a " + "name with no referent; this looks for a referent " + "with no name, which is the direction #7616/#7453 " + "came from. Takes no corpus.") ap.add_argument("--audit-immovable-sources", action="store_true", help="re-check the PREMISES of every --unrooted-allocas " "exemption against the runtime source (#7210): the " @@ -4866,6 +5257,8 @@ def main(): return audit_alloc_re() if ns.audit_poll_capable: return audit_poll_capable() + if ns.audit_poll_reach: + return audit_poll_reach() if ns.audit_immovable_sources: return audit_immovable_sources() From e14bac9662e9ddd2506a488c32b3de481b44be50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 11:35:26 +0200 Subject: [PATCH 2/2] chore: bump version to 0.5.1391 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5a4522904e..1691eff0b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1390 +**Current Version:** 0.5.1391 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index d37c4c9b50..cd020231df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1390" +version = "0.5.1391" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1390" +version = "0.5.1391" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1390" +version = "0.5.1391" [[package]] name = "perry-ui-tvos" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1390" +version = "0.5.1391" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index e2bf66bf27..9a63ca196a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1390" +version = "0.5.1391" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"