-
-
Notifications
You must be signed in to change notification settings - Fork 161
fix(runtime): resolve symbol props written to declared class prototypes from instances #10250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
proggeramlug
wants to merge
1
commit into
PerryTS:main
from
proggeramlug:fix/class-prototype-symbol-props
+167
−0
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Resolve symbol-keyed properties that were added to a declared class's `.prototype` after the declaration (`C.prototype[S] = f`, `Object.defineProperty(C.prototype, S, ...)`, `Object.assign(C.prototype, { [Symbol.iterator]() {} })`) from instances and subclass instances. The symbol read fallback only walked the dynamic prototype-object table (`Object.create` / class-expression parents) and never the declared prototype object those writes land on, so `typeof instance[Symbol.iterator]` was `undefined` and `Symbol.iterator in instance` was false while the same string-keyed writes worked. drizzle-orm's `applyEffectWrapper` installs effect's `Effectable.Prototype` on its query classes this way, so every `yield* db.run(...)` in OpenCode's database layer failed with "next is not a function" (tracker #10107). |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| //! Symbol-keyed properties added to a declared class's `.prototype` after the | ||
| //! declaration (`C.prototype[S] = f`, `Object.defineProperty(C.prototype, S, ...)`, | ||
| //! `Object.assign(C.prototype, { [Symbol.iterator]() {} })`) must be visible on | ||
| //! instances and subclass instances, exactly like string keys. drizzle-orm's | ||
| //! `applyEffectWrapper` installs effect's `Effectable.Prototype` this way, and | ||
| //! `yield* query` needs the copied `[Symbol.iterator]`. | ||
|
|
||
| use std::path::PathBuf; | ||
| use std::process::Command; | ||
| use std::sync::Once; | ||
|
|
||
| fn perry_bin() -> PathBuf { | ||
| PathBuf::from(env!("CARGO_BIN_EXE_perry")) | ||
| } | ||
|
|
||
| fn workspace_root() -> PathBuf { | ||
| PathBuf::from(env!("CARGO_MANIFEST_DIR")) | ||
| .join("../..") | ||
| .canonicalize() | ||
| .expect("canonicalize workspace root") | ||
| } | ||
|
|
||
| fn runtime_dir() -> PathBuf { | ||
| static BUILD_RUNTIME: Once = Once::new(); | ||
| BUILD_RUNTIME.call_once(|| { | ||
| let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); | ||
| let build = Command::new(cargo) | ||
| .current_dir(workspace_root()) | ||
| .arg("build") | ||
| .arg("-p") | ||
| .arg("perry-runtime-static") | ||
| .arg("-p") | ||
| .arg("perry-stdlib-static") | ||
| .output() | ||
| .expect("build static runtime archives"); | ||
| assert!( | ||
| build.status.success(), | ||
| "static runtime build failed\nstdout:\n{}\nstderr:\n{}", | ||
| String::from_utf8_lossy(&build.stdout), | ||
| String::from_utf8_lossy(&build.stderr) | ||
| ); | ||
| }); | ||
| let target = std::env::var_os("CARGO_TARGET_DIR") | ||
| .map(PathBuf::from) | ||
| .unwrap_or_else(|| workspace_root().join("target")); | ||
| target.join("debug") | ||
| } | ||
|
|
||
| const SOURCE: &str = r#" | ||
| const t = (name: string, f: () => any) => { try { console.log(name, JSON.stringify(f())) } catch (e: any) { console.log(name, "THROW", e.message) } } | ||
| const fn = function (this: any) { return "called" } | ||
| const custom = Symbol("custom") | ||
| class A1 {}; (A1.prototype as any)[Symbol.iterator] = fn | ||
| t("P1", () => typeof (new A1() as any)[Symbol.iterator]) | ||
| class A2 {}; Object.assign(A2.prototype, { [Symbol.iterator]: fn }) | ||
| t("P2", () => typeof (new A2() as any)[Symbol.iterator]) | ||
| class A3 {}; Object.defineProperty(A3.prototype, Symbol.iterator, { value: fn, writable: true, configurable: true }) | ||
| t("P3", () => typeof (new A3() as any)[Symbol.iterator]) | ||
| class A4 {}; Object.assign(A4.prototype, { [custom]: fn }) | ||
| t("P4", () => (new A4() as any)[custom]()) | ||
| class A5 {}; (A5.prototype as any)[Symbol.iterator] = fn | ||
| class A5s extends A5 {} | ||
| t("P5", () => typeof (new A5s() as any)[Symbol.iterator]) | ||
| t("P6", () => Symbol.iterator in (new A2() as any)) | ||
| class A7 {}; Object.defineProperty(A7.prototype, custom, { get(this: any) { return this.v }, configurable: true }) | ||
| t("P7", () => { const a: any = new A7(); a.v = 7; return a[custom] }) | ||
| class A8 { [Symbol.iterator]() { return "declared" } } | ||
| t("P8", () => (new A8() as any)[Symbol.iterator]()) | ||
| class SingleShotGen { called = false; constructor(public self: any) {} next(a?: any) { return this.called ? { value: a, done: true } : ((this.called = true), { value: this.self, done: false }) } } | ||
| const EffectProto: any = { op: "E", [Symbol.iterator]() { return new SingleShotGen(this) } } | ||
| class Raw { constructor(public execute: () => number) {} } | ||
| Object.assign(Raw.prototype, { ...EffectProto }) | ||
| class SubRaw extends Raw {} | ||
| t("Y1", () => { const r = new Raw(() => 1); function* g(): Generator<any, any, any> { const x = yield* r; return x } const it = g(); const a = it.next(); const b = it.next("sent"); return [a.value === r, b.value, b.done] }) | ||
| t("Y2", () => { const r = new SubRaw(() => 2); function* g(): Generator<any, any, any> { return yield* r } const it = g(); it.next(); return it.next("sub").value }) | ||
| class It {}; (It.prototype as any)[Symbol.iterator] = function* () { yield 1; yield 2 } | ||
| t("Y3", () => { const out: number[] = []; for (const v of new It() as any) out.push(v); return [out, [...(new It() as any)]] }) | ||
| "#; | ||
|
|
||
| const EXPECTED: &str = "P1 \"function\" | ||
| P2 \"function\" | ||
| P3 \"function\" | ||
| P4 \"called\" | ||
| P5 \"function\" | ||
| P6 true | ||
| P7 7 | ||
| P8 \"declared\" | ||
| Y1 [true,\"sent\",true] | ||
| Y2 \"sub\" | ||
| Y3 [[1,2],[1,2]] | ||
| "; | ||
|
|
||
| #[test] | ||
| fn symbol_props_written_to_declared_class_prototypes_reach_instances() { | ||
| let dir = tempfile::tempdir().expect("tempdir"); | ||
| let entry = dir.path().join("main.ts"); | ||
| let output = dir.path().join("main_bin"); | ||
| std::fs::write(&entry, SOURCE).expect("write entry"); | ||
| let compile = Command::new(perry_bin()) | ||
| .current_dir(dir.path()) | ||
| .arg("compile") | ||
| .arg(&entry) | ||
| .arg("-o") | ||
| .arg(&output) | ||
| .arg("--no-cache") | ||
| .env("PERRY_NO_AUTO_OPTIMIZE", "1") | ||
| .env("PERRY_RUNTIME_DIR", runtime_dir()) | ||
| .output() | ||
| .expect("run perry compile"); | ||
| assert!( | ||
| compile.status.success(), | ||
| "perry compile failed\nstdout:\n{}\nstderr:\n{}", | ||
| String::from_utf8_lossy(&compile.stdout), | ||
| String::from_utf8_lossy(&compile.stderr) | ||
| ); | ||
| let run = Command::new(&output) | ||
| .current_dir(dir.path()) | ||
| .output() | ||
| .expect("run compiled binary"); | ||
| assert!( | ||
| run.status.success(), | ||
| "compiled binary failed\nstdout:\n{}\nstderr:\n{}", | ||
| String::from_utf8_lossy(&run.stdout), | ||
| String::from_utf8_lossy(&run.stderr) | ||
| ); | ||
| assert_eq!(String::from_utf8_lossy(&run.stdout), EXPECTED); | ||
| } |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Resolve prototype writes before stale declared symbol members.
This fallback runs after
class_symbol_getter_valueandlookup_class_symbol_method_in_chain. Those branches return the original declared member first.For example,
C.prototype[S] = replacementcannot replace a custom[S]()method or getter declared inC. The instance still reads the original member.Merge declared-prototype and registered-member lookup per class. Check the nearest class first. Within each class, let the current own prototype property shadow its registered declaration. Preserve subclass precedence over ancestor properties. Add method and accessor replacement cases to the integration test.
🤖 Prompt for AI Agents