Repository navigation
wpfacts: a promise resolve function adopts a thenable (class F of the activation false proofs; one rule, Cons::Fulfil) - #14569
Merged
Conversation
added 5 commits
November 12, 2026 17:40
…::Fulfil) A promise is never fulfilled with a thenable (ECMA-262 27.2.1.3.2, 27.2.2.2). The solve's resolve function copied its argument as it is, so a thenable given to the executor's resolve (or to the resolve function a thenable's then gets) never had its then called: that body was called inactive while it ran (check mode: Activation / inactive body: body entered on promise/test_gap_promise_tick_parity and promise/test_new_promise_only_thenable; PERRY_FACTS_DCE=1 trapped). One constraint, Cons::Fulfil, decides from the value's then read: a primitive, an unknown value, a marker, or an object whose then read may hold a value no call takes (guard::NOT_CALLABLE) fulfils as itself; a callable then adopts it (its settled value, its then called with resolve functions), re-checked as the read grows; a native promise reads its locked Promise.prototype.then without the chain end's undefined (locked_chain_defines). builtins::adopt, the resolve function (resolver, memoized before it adopts) and Gen::awaited share it; a reject function (rejecter) keeps its plain edge. The rule is the solver lane's commit c5b8b61, ported to main. PERRY_FACTS_REACH_THENABLE=0 restores the previous model.
…henable was reached before (adopt, then the derived promise's awaited value); the hole is the executor's resolve
…-token read node is a planned follow-up
|
Caution Review failedThe pull request is closed.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Class F (the last solver-side class but H) of the
Activation / inactive body: body enteredfalse proofs of check mode over the full parity corpus. The rule is the solver lane's commit c5b8b61, ported to main with a switch; the differences are listed below.The hole
A promise is never fulfilled with a thenable (ECMA-262 27.2.1.3.2 Promise Resolve Functions steps 9-15, 27.2.2.2 NewPromiseResolveThenableJob): resolving with an object whose
thenis callable calls thatthenwith fresh resolve functions. The solve's resolve function (builtins::resolver, the one the executor ofnew Promisereceives and the one a thenable'sthenreceives) copied its argument into the settled value as it is, sonever called the thenable's
then: that body was "inactive" while it ran. Check mode rc 86 onpromise/test_gap_promise_tick_parityandpromise/test_new_promise_only_thenable(the latter on purpose since #14538); withPERRY_FACTS_DCE=1the removed body traps (SIGILL). The paths throughbuiltins::adopt(Promise.resolve,await, a handler's result) already calledthen, but also let the thenable itself flow on as the settled value.The fix (the lane's rule; in the one solve)
Cons::Fulfil { dst }, decides from the value'sthenread: a primitive, ⊤, a marker, or an object token whosethenread may hold a value no call takes (guard::NOT_CALLABLE: a primitive,undefinedof a missing key, a non-function object, ⊤;builtins::surely_callable) fulfils as itself; an object whosethenis callable is adopted instead (its settled value, itsthencalled with resolve functions), re-checked as the read grows (monotone). A native promise reads its lockedPromise.prototype.thenwithout the chain end'sundefined(builtins::locked_chain_defines), its ownthenvalues as they are.builtins::adopt, the resolve function (builtins::resolver, whose argument is now adopted; memoized BEFORE it adopts, sinceadoptasks for the same resolver) andGen::awaited(a second copy of the rule with a plain edge) share it. A reject function (builtins::rejecter, its own memo) keeps the plain edge: a reject function adopts nothing.thenis called) and its settled values flow on. A value fulfils "as itself" only under the lane's condition; no other shortcut.PERRY_FACTS_REACH_THENABLE=0= the previous model (reach.rs; in the compile env list, sorted;reach_thenable_switch_is_a_cache_input; test controlnoreachthenable).Differences from the lane commit c5b8b61
reach::thenable,Model::reach_thenable, lib.rs, the compile env entry and its test. Off arms:resolver/rejecter->plain_resolver(main's oldresolver, one memo, name<resolver>),adoptkeepsm.edge(v, dst),Gen::awaited->awaited_plain(main's body). Reason: lane rule, off = today's main.#valuefield (m.atom("#value"), unchanged lines), not the lane'spromise_result_atom([[PromiseResult]] internal slot, lane commits 1852863 / 82bee0c). Reason: lane-only infrastructure.Model::fieldinstead ofModel::field_rin theFulfilapply arm (ownthenvalues of a native promise). Reason:field_r(lane 0b775b7) isfieldwithout the ENUM3 enumerable-key mark, which main does not have.guard::NOT_CALLABLEkeeps the lane's value 11 although main's last guard isBOXED = 7(the lane's 8-10,BOXED_STR/BOOL/OTHERfrom 16cb2be, are not on main), so the lane's next merge with main meets the same constant.Cons::Fulfilmatch arms sit besideNotNullish; in the lane they sit besideFieldTruthy(lane 5e514c7, not on main).Gen::awaitedis in gen_protocol.rs (moved from gen_expr.rs by wpfacts: move the promise and iteration protocol helpers out of gen_expr.rs (pure move, file size cap) #14525);Gen::resolver_fnis removed as in the lane (its only use wasawaited; the off arm callsplain_resolver).Cons::Fulfildoc says "its settled value" for "its [[PromiseResult]]" (item 2).Cons::Load { .. }lines is not applied (the lane's own text).tests/reach_thenable.rs(activation controls), the parity fixture, the changelog fragment, the updated comment inpromise/test_new_promise_only_thenable.ts.Notes (solver lane)
#valueFIELD, which a spread, an unknown-key read and a for-in can reach:Object.assign({}, promise)or{...promise}copies the settled value, and awaiting that copy yields it. Adoption does not make this worse. This PR is adoption only, not adoption plus internal-slot hygiene; the internal-slot fix ([[PromiseResult]]) comes with the solver lane's head.Fulfilarm makes two nodes per (Fulfil constraint, token) and is applied on deltas. Measured solve-size deltas are in "Measured" below. The solver's owner accepted this cost for this PR and plans a per-token read node (onethenread per token instead of per (constraint, token)) as a follow-up; a comment at the arm says so.Solver files touched (owner CWN; the lane's code)
crates/perry-wpfacts/src/builtins.rs595-660 (resolver,rejecter,plain_resolver,settle_fn), 679, 742-757 (adopt), 946-954 (locked_chain_defines)builtins/constructors.rs57, 59;builtins/methods.rs694-710 (surely_callable)gen_protocol.rs16-80 (Gen::awaited,awaited_plain)model.rs86-90, 104 (guard::NOT_CALLABLE), 1830-1832, 1876-1877;model/cons.rs241-252, 659, 677, 717solve/apply.rs417-418 (Guard), 450-493 (theFulfilarm)demand.rs139,records.rs246-247 / 392 / 1842,solve.rs294 / 1685,summary.rs277,summary/map.rs120 / 481,summary/packet/symbols.rs145,zero_sign.rs124reach.rs70-80 (switch),lib.rs588Fixtures
tests/promise_adopts_thenable.rs: the lane's 8 tests, unchanged (a then-callback's promise, a resolve function given a promise,awaitof a returned promise, a program thenable whosethenruns with the runtime resolve function, athen: 5object fulfils as itself; controls: an unknown ownthen, an unknown executor, an unknown value stay open).tests/reach_thenable.rs: 5 programs where a markerhitruns only through a thenable'sthenreached by a resolve function (the executor's resolve, awaited, a kept resolve called later, a thenable that resolves a thenable, what a thenable resolves flowing on): active with the rule, inactive withnoreachthenable(the control); 3 programs reached before throughadopt(Promise.resolve,await, a handler's result): active both ways.tests/parity/language/promise/test_reach_thenable_adoption.ts(13 cases incl. reject / throwingthen, a native promise, a class thenable, and negative controlsthen: 5, nothen, athengetter returningundefined, a primitive).Measured (qb10; build of 48f1043 on main 8654fbb; this PR's switch is the arm)
The head differs from the measured build by a test-list fix, a comment line at the Fulfil arm, and merges of main (cargo check of perry-wpfacts lib + tests and perry tests, and check_file_size, green after the last merge).
unit_mode::contracts::a_broken_namespace_field_function_resolves_only_the_units_that_see_it(red on main) and one case of this PR's own control module (moved to the covered list; rerun on abd6978:reach_thenable2/0,promise_adopts_thenable8/0, filterspromise21/0,resolver4/0,thenable15/0,await2/0,builtins13/0).tests/num_seeds_audit: red on main as well (main'sprim::NUMcounts already differ from NUM_SEEDS.md for bindings.rs, quiet_sites.rs, solve/apply.rs); this PR adds noprim::NUMuse. perrycompile_env44 / 0.check_file_size.shgreen. Release build: 4 Rust warnings, all runtime, a subset of main's (none added).promise/test_gap_promise_tick_parity,promise/test_new_promise_only_thenable,promise/test_thenable_resolution_paths): on = rc 0 and = node in default mode, check mode (0 false proofs) and withPERRY_FACTS_DCE=1. WithPERRY_FACTS_REACH_THENABLE=0: check mode rc 86 (Activation) andPERRY_FACTS_DCE=1rc 132 (SIGILL) on r09, r09c, the new fixture, tick_parity and new_promise_only_thenable, as on main; r09b (theawaitform) and test_thenable_resolution_paths are clean either way.class/test_class_key_order_first_store(perf(layout): a class's keys are laid out in the order the constructor first stores them #14485 key order),object/test_vr_replacer_hole_main(JSON replacer hole),temporal/test_gap_temporal_keyed_read_chain(class H). Off = 4 (+promise/test_gap_promise_tick_parity). On vs off, and on vs the reach stack's run, the only (compile rc, run rc) change is tick_parity 86 -> 0; no program entered; the same 5 compile failures.perf stat -e instructions:u): byte-identical binary (sha256 8cb24f6b62482c6a, 9 365 576 B).Chess check mode: 0 misses on all four workloads.
thenload each; relations S guard +992, S load +1 004, S defined +224); the settled values of three solver-born promise templates (Promise@call4086 / 4312 / 4349, 18 Fulfil constraints each) hold 595 of them (266 / 192 / 137 tokens); gen-time each resolver now adopts (athencall with fresh resolve / reject nodes: B mcall +135, new nodes +1 335). The "fulfilled as itself" condition is the lane's, unchanged. Accepted by the solver's owner; the per-token read is a planned follow-up.