fix: functional-correctness batch 1 (semver.sort, immer hasOwnProperty, ctor return-override, jwt crypto keys)#5386
Conversation
A method named `sort` on a class instance or an imported module-exports object (semver re-exports `sort = (list) => list.sort(cmp)`, called as `semver.sort(list)`) was unconditionally folded into `Expr::ArraySort`, mis-routing the single `list` argument into the comparator slot. The runtime comparator validator then saw the array (not a function) and threw "The comparison function must be either a function or undefined: ...". Gate the `sort` fold on the receiver being a known array, mirroring the existing map/filter/join guards: - array_only_methods.rs: require !recv_is_class for the `sort` arm, and bail for imported-binding receivers (treated like namespaces). - imported_array_methods.rs: only fold when the imported binding's return_type is statically Array (mirrors the #420 `join` fix). - local_array_methods.rs: skip the fold for known class-instance receivers.
… constructor `Object` is a function whose member reads resolve up the prototype chain, so `Object.hasOwnProperty` IS `Object.prototype.hasOwnProperty` (a callable). immer's `isPlainObject` does `O.hasOwnProperty.call(proto, CONSTRUCTOR)` with `const O = Object`; the reified Object constructor value had only its static methods installed, so reading `hasOwnProperty` returned `undefined` and `.call` threw "Function.prototype.call was called on a value that is not a function". Install the Object.prototype methods reachable on the constructor by inheritance (hasOwnProperty / isPrototypeOf / propertyIsEnumerable / toString / toLocaleString / valueOf) as statics on the reified ctor, reusing the existing `object_prototype_*_thunk` impls (which read the receiver from IMPLICIT_THIS, so `.call(receiver, ...)` binds correctly).
…bol new path
ECMAScript: a constructor that explicitly returns an object (or function)
makes `new C()` evaluate to that value, not the freshly-allocated `this`. The
default codegen path calls a shared standalone `<class>_constructor` symbol
(the inline path is opt-in via PERRY_INLINE_CTOR=1); that path discarded the
ctor's return value and always returned `obj_box`, so a constructor like
chalk's `class Chalk { constructor(o){ return chalkFactory(o); } }`
(no-constructor-return) produced the empty default instance instead of the
returned factory function.
call_local_constructor_symbol now returns the standalone ctor's call result;
the new site feeds it through js_ctor_return_override (the same helper the
inline path already used). The standalone ctor returns `undefined` for an
ordinary body, and js_ctor_return_override maps `undefined`/primitive back to
`obj_box`, so ordinary constructors are unaffected (verified: import-smoke
59/59, functional OK count unchanged).
…terial
Node throws when createPrivateKey/createPublicKey receive input that is not
valid key material (e.g. a plain non-PEM string) instead of producing a
KeyObject with `type === undefined`. perry returned the raw string as a key,
so its `.type` was undefined. jsonwebtoken depends on the throw:
sign()/verify() do `try { createPrivateKey(secret) } catch { createSecretKey(...) }`,
so a string HMAC secret must make createPrivateKey/createPublicKey throw to
reach the symmetric-key fallback — otherwise HS256 signing reported
"secretOrPrivateKey must be a symmetric key" and verify reported "invalid
algorithm".
js_crypto_create_private_key_value / js_crypto_create_public_key_value now
throw (ERR_OSSL_PEM_NO_START_LINE) unless the input classifies as a real
key surrogate or carries a PEM `-----BEGIN ... KEY-----` header. The
legitimate createSecretKey(Buffer) path is unaffected (still type 'secret').
No functional/import-smoke regressions.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughFour independent correctness fixes: (1) the standalone ChangesConstructor Return Override (Standalone Path)
ArraySort Mis-folding Fixes
Object Constructor Prototype Method Reification
Crypto Key Creation Error Throwing
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Functional-correctness batch 1
A new differential harness (
secret-tests/corpus/functional/) byte-diffs real package APIs betweennode --experimental-strip-typesand perry-source-compiled drivers. This PR fixes four DISTINCT functional-correctness roots surfaced by it, each with a regression test. The packages whose root I fixed advance past their original failure (some then hit further independent bugs, noted below).Fixed (root cause per bug)
semver.sort —
array_only_methods.rs/imported_array_methods.rs(+local_array_methods.rs) unconditionally folded a.sort(list)method call into theExpr::ArraySortintrinsic even when the receiver was a class instance or an imported module-exports object. semver re-exportssort = (list) => list.sort(cmp)and the driver callssemver.sort(list); the fold mis-routed the singlelistarg into the comparator slot → "The comparison function must be either a function or undefined". Fix: gate thesortfold on a real-array receiver (mirrors the existing map/filter/join!recv_is_class/ Array-return-type guards). semver now sorts correctly; its driver still DIFFs on a separatesatisfies/maxSatisfyingRange-test bug (not addressed).immer
O.hasOwnProperty.call(...)—Objectis a function, so member reads resolve up its prototype chain:Object.hasOwnPropertyISObject.prototype.hasOwnProperty(callable). The reifiedObjectconstructor value only had its static methods installed, soconst O = Object; O.hasOwnPropertyreturnedundefined→.call(...)threw "Function.prototype.call on non-function". Fix: install the inherited Object.prototype methods (hasOwnProperty / isPrototypeOf / propertyIsEnumerable / toString / toLocaleString / valueOf) on the reified ctor, reusing the existingobject_prototype_*_thunkimpls. immer's hasOwnProperty path fixed; its driver then hits a separatereading 'slice'bug.chalk constructor return-override (codegen,
newpath) — ECMAScript: a constructor that explicitly returns an object/function makesnew C()evaluate to that value. The defaultnewcodegen calls a shared standalone<class>_constructorsymbol and DISCARDED its return value, always yielding the empty default instance. chalk'sclass Chalk { constructor(o){ return chalkFactory(o); } }produced the default instance instead of the factory function. Fix: capture the standalone ctor's return value and run it throughjs_ctor_return_override(the same helper the inline path already used;undefined/primitive maps back tothis, so ordinary ctors are unaffected). Verified by fixtures (object- and function-returning ctors); chalk itself still needs further prototype-getter machinery, so its driver still DIFFs.jsonwebtoken
createPrivateKey/createPublicKey(crypto) — Node throws on input that is not valid key material; perry returned the raw string as atype: undefinedkey. jsonwebtoken doestry { createPrivateKey(secret) } catch { createSecretKey(...) }, so a string HMAC secret must make those throw to reach the symmetric-key fallback — otherwise HS256 sign reported "secretOrPrivateKey must be a symmetric key" and verify reported "invalid algorithm". Fix: throw (ERR_OSSL_PEM_NO_START_LINE) unless the input classifies as a real key surrogate or carries a PEM-----BEGIN ... KEY-----header. Fixes the symmetric-key + invalid-algorithm errors; jwt then hits a furtherreading 'prototype'bug in the jws layer.Characterized only (not fixed)
z.string()→core._string(ZodString, …)→new Class(…)throws "undefined is not a constructor" becauseZodString(aconst = core.$constructor("ZodString", …)) is undefined at the call site. A module-init-order / ≥300-const-init scale issue (known hard zod blocker class, cf. perry-codegen: Effect — (number).slice is not a function during Schema.ts__init (#680 follow-up, ~310th init) #684/tests: harness for dynamic class extension / mixins #806). Isolated fixtures of the$constructor/new Cls(param)pattern work; the failure is the cross-module const value being undefined.internals.generatereadsschema._definition.argswhereschema._definitionis undefined: the type schema object's_definitionfield isn't initialized. Deep joi internals.marked.parse(md)returns an opaque object (no keys, no ctor, not a Promise) instead of an HTML string, through marked's nested class-field-closure parse chain. Not localized to a single root._.get(obj, "a.b.c")throws "reading 'size'" inside lodash'sMapCache.set(getMapData(this, key)returns undefined). Faithful standalone MapCache fixtures work, so the trigger is more subtle (likely the cached-native-Map/ListCacheselection).new Minimatch(p, o)instances have nomatchmethod and nosetfield (prototype methods /this.make()-set fields missing). Class-method/field resolution gap for this cross-module class.RUST_MIN_STACKineffective). Unbounded recursion in a codegen lowering/type pass; specific trigger construct not yet isolated.Results
cargo testperry-hir / perry-codegen / perry-stdlib(crypto): all green. fmt/file-size/gc-inventory/addr-class lints clean.tests/test_*.shregression tests.Version / changelog
Per the batch instructions, this PR does not bump
Cargo.toml/CLAUDE.mdversion or touchCHANGELOG.md— maintainer folds metadata at merge.Summary by CodeRabbit
sortmethods into array intrinsics, preventing argument mis-routing.Object.prototypemethods (hasOwnProperty,isPrototypeOf, etc.) via theObjectconstructor.