-
-
Notifications
You must be signed in to change notification settings - Fork 158
fix(error): fs diagnostics become own properties of the error, not of its message string #8889
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| Fixed an fs error's `code`/`errno`/`syscall`/`path`: they are now own properties | ||
| of the **error object** rather than entries in side tables keyed by the error's | ||
| **message string address**. | ||
|
|
||
| Two defects followed from that keying, both verified against node. | ||
|
|
||
| **The wrong error got the metadata.** Any `new Error(m)` built from the same | ||
| message text inherited an unrelated fs error's fields — `.code` returned | ||
| `ENOENT` where node returns `undefined`, along with `.syscall`, `.errno` and | ||
| `.path`. The metadata belonged to the string, so anything holding that string | ||
| answered to it. | ||
|
|
||
| **They were invisible to reflection.** In node these are ordinary own | ||
| properties; served from a side table behind property *getters* they appeared in | ||
| none of the enumeration paths: | ||
|
|
||
| | | before | after (= node) | | ||
| |---|---|---| | ||
| | `Object.keys(e)` | `[]` | `code,errno,path,syscall` | | ||
| | `hasOwnProperty('code')` | `false` | `true` | | ||
| | `getOwnPropertyDescriptor` | `undefined` | `{value,writable,enumerable,configurable}` | | ||
| | `JSON.stringify(e)` | `{}` | `{"errno":-2,"code":"ENOENT",…}` | | ||
| | `{...e}` | `{}` | same | | ||
|
|
||
| Any code that logged or serialised a caught fs error lost its whole payload. | ||
|
|
||
| Three sites each held a different wrong assumption about errors. The fs builders | ||
| keyed on the message string. `JSON.stringify` hardcoded `"{}"` for | ||
| `GC_TYPE_ERROR` — correct for a *plain* error, whose `message`/`name`/`stack` | ||
| are non-enumerable, but wrong once an error carries enumerable own properties, | ||
| so it also dropped **user-assigned** ones (`e.foo=1; JSON.stringify(e)` gave | ||
| `{}` where node gives `{"foo":1}`; that half is independent of fs). | ||
| `Object.assign`/spread had no Error arm and copied nothing. All three now | ||
| enumerate through `exotic_own_keys(.., enumerable_only = true)` — the same | ||
| enumeration `Object.keys` uses — so they cannot drift apart again. | ||
|
|
||
| Property **order** is fixed too. `ERROR_USER_PROPS` was a `HashMap` with an | ||
| alphabetical `sort_by` bolted on for determinism: stable, but not node's. Own | ||
| string keys enumerate in insertion order per ECMA-262, and that order is | ||
| observable through all four paths above. The store is insertion-ordered now, | ||
| reassignment keeps a key's original position (`o.a=1; o.b=2; o.a=3` enumerates | ||
| `a,b`), and the fs fields install in node's `uvException` order. The GC root | ||
| scanner over these properties moved to the ordered store. | ||
|
|
||
| Verified by running three repro programs against node on the same host: | ||
| byte-identical output, key order included. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,54 @@ pub(crate) fn io_error_errno(err: &std::io::Error) -> i32 { | |
| } | ||
| } | ||
|
|
||
| /// Attach Node's fs diagnostic fields to `err_ptr` as **own properties of the | ||
| /// error object**. | ||
| /// | ||
| /// These used to be registered in six side tables keyed by the MESSAGE | ||
| /// STRING's address (`register_error_code_pub` and friends), which produced two | ||
| /// defects: | ||
| /// | ||
| /// * **Wrong error.** Any `new Error(m)` built from the same message text | ||
| /// inherited the unrelated fs error's fields — `new Error(e.message).code` | ||
| /// returned `ENOENT` where node returns `undefined`, along with `.syscall`, | ||
| /// `.errno` and `.path`. Metadata belonged to the string, not the throw. | ||
| /// * **Invisible to reflection.** In node these are ordinary own properties: | ||
| /// `Object.keys(e)` is `code,errno,path,syscall`, and `JSON.stringify(e)` and | ||
| /// `{...e}` carry them. Served from a side table behind property *getters* | ||
| /// they were absent from all of it — perry returned `{}` for both, so any | ||
| /// code that logs or serialises an fs error silently lost every field. | ||
| /// | ||
| /// Keying on the error object fixes both at once, and each field then reaches | ||
| /// reflection through the same path a user assignment does. | ||
| unsafe fn attach_fs_error_props( | ||
| err_ptr: *mut crate::error::ErrorHeader, | ||
| code: &str, | ||
| errno: i32, | ||
| syscall: &str, | ||
| path: Option<&str>, | ||
| dest: Option<&str>, | ||
| ) { | ||
| use crate::node_submodules::set_error_user_prop; | ||
| let owner = err_ptr as usize; | ||
| let put_str = |key: &str, s: &str| { | ||
| let boxed = js_string_from_bytes(s.as_ptr(), s.len() as u32); | ||
| set_error_user_prop(owner, key, crate::value::js_nanbox_string(boxed as i64)); | ||
| }; | ||
| // Insertion order is observable — `Object.keys`, `for…in`, `{...err}` and | ||
| // `JSON.stringify` all report it — so install these in the same order | ||
| // node's `uvException` does: errno, code, syscall, path, dest. | ||
| // `errno` is numeric in node (-2 for ENOENT), not a string. | ||
| set_error_user_prop(owner, "errno", errno as f64); | ||
| put_str("code", code); | ||
| put_str("syscall", syscall); | ||
| if let Some(p) = path { | ||
| put_str("path", p); | ||
| } | ||
| if let Some(d) = dest { | ||
| put_str("dest", d); | ||
| } | ||
|
Comment on lines
+124
to
+150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository guidance ---'
for f in /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc/*/*.md; do
case "$f" in
*perry-runtime*|*learnings*) printf '\n--- %s ---\n' "$f"; head -200 "$f";;
esac
done
printf '%s\n' '--- changed and directly bound code ---'
sed -n '90,175p' crates/perry-runtime/src/fs/errors.rs
sed -n '330,425p' crates/perry-runtime/src/json/stringify.rs
sed -n '1515,1605p' crates/perry-runtime/src/object/alloc.rs
printf '%s\n' '--- handle and expando definitions ---'
rg -n -A35 -B15 'fn (exotic_get_own_property|exotic_own_keys)|struct RuntimeHandleScope|root_raw_mut_ptr|root_nanbox_f64|set_error_user_prop|attach_fs_error_props' crates/perry-runtime/srcRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository guidance filenames ---'
find /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc -type f -maxdepth 3 -print
printf '%s\n' '--- relevant guidance ---'
find /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc -type f -maxdepth 3 -print0 |
xargs -0 grep -l 'crates/perry-runtime\|fs/errors\|json/stringify\|object/alloc' |
while read -r f; do printf '\n--- %s ---\n' "$f"; cat "$f"; done
printf '%s\n' '--- reviewed functions ---'
sed -n '105,165p' crates/perry-runtime/src/fs/errors.rs
sed -n '360,415p' crates/perry-runtime/src/json/stringify.rs
sed -n '1540,1600p' crates/perry-runtime/src/object/alloc.rs
printf '%s\n' '--- exact definitions and callers ---'
rg -n -A25 -B12 'pub unsafe fn set_error_user_prop|fn set_error_user_prop|unsafe fn exotic_get_own_property|fn exotic_get_own_property|unsafe fn exotic_own_keys|fn exotic_own_keys|pub struct RuntimeHandleScope|impl.*RuntimeHandleScope|root_raw_mut_ptr|root_nanbox_f64' crates/perry-runtime/src/fs crates/perry-runtime/src/object crates/perry-runtime/src/json crates/perry-runtime/src/gc.rsRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- reviewed functions ---'
sed -n '115,165p' crates/perry-runtime/src/fs/errors.rs
sed -n '365,415p' crates/perry-runtime/src/json/stringify.rs
sed -n '1550,1600p' crates/perry-runtime/src/object/alloc.rs
printf '%s\n' '--- exact bound definitions ---'
rg -n -A30 -B8 '^pub unsafe fn set_error_user_prop|^pub\(crate\) unsafe fn set_error_user_prop|^unsafe fn set_error_user_prop|^pub fn set_error_user_prop|^fn set_error_user_prop' crates/perry-runtime/src
rg -n -A35 -B8 'pub struct RuntimeHandleScope|impl.*RuntimeHandleScope|fn root_raw_mut_ptr|fn root_nanbox_f64' crates/perry-runtime/src/gc.rs
rg -n -A45 -B8 'fn exotic_get_own_property|pub.*exotic_get_own_property|fn exotic_own_keys|pub.*exotic_own_keys' crates/perry-runtime/src/object/exotic_expando.rs
printf '%s\n' '--- only relevant repository guidance files ---'
find /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc -type f -print0 |
while IFS= read -r -d '' f; do
if grep -qE 'perry-runtime|fs/errors|json/stringify|object/alloc' "$f"; then
printf '\n--- %s ---\n' "$f"
grep -n -A12 -B3 -E 'perry-runtime|fs/errors|json/stringify|object/alloc' "$f"
fi
doneRepository: PerryTS/perry Length of output: 10999 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- GC source locations ---'
fd -t f 'gc|exotic_expando|diagnostics' crates/perry-runtime/src
printf '%s\n' '--- RuntimeHandleScope implementation ---'
rg -n -A45 -B10 'pub struct RuntimeHandleScope|impl.*RuntimeHandleScope|root_raw_mut_ptr|root_raw_const_ptr|root_nanbox_f64|across_mut' crates/perry-runtime/src/gc
printf '%s\n' '--- Error exotic operations ---'
rg -n -A70 -B15 'exotic_get_own_property|exotic_own_keys' crates/perry-runtime/src/object/exotic_expando
printf '%s\n' '--- Error allocation and moving behavior ---'
rg -n -A35 -B15 'js_error_new_with_message|GC_TYPE_ERROR|obj_type.*GC_TYPE_ERROR|GC_HEADER_SIZE|evacu|move' crates/perry-runtime/src/error crates/perry-runtime/src/gc crates/perry-runtime/src/node_submodules/diagnostics.rsRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- GC files ---'
find crates/perry-runtime/src -maxdepth 3 -type f -name '*.rs' | grep '/gc'
printf '%s\n' '--- handle API ---'
rg -n -A30 -B8 'pub struct RuntimeHandleScope|impl RuntimeHandleScope|root_raw_mut_ptr|root_raw_const_ptr|root_nanbox_f64|across_mut' crates/perry-runtime/src/gc
printf '%s\n' '--- moving collector contract ---'
rg -n -A25 -B10 'rewrite.*root|forwarding|copying|relocat|transient|raw.*pointer|raw pointer' crates/perry-runtime/src/gc | head -500
printf '%s\n' '--- enclosing object assignment function ---'
sed -n '1450,1605p' crates/perry-runtime/src/object/alloc.rs
printf '%s\n' '--- stringify callers ---'
rg -n -A15 -B15 'stringify_error_own_props' crates/perry-runtime/src/json/stringify.rsRepository: PerryTS/perry Length of output: 50372 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Error allocation path ---'
rg -n -A45 -B12 'js_error_new_with_message' crates/perry-runtime/src
printf '%s\n' '--- GC root scanner and relocation rules ---'
rg -n -A35 -B12 'scan_runtime_handle_roots_mut|RawTagged|POINTER_TAG|rewrite.*slot|forwarding_address' crates/perry-runtime/src/gc/roots crates/perry-runtime/src/gc | head -300
printf '%s\n' '--- exotic implementation only ---'
rg -n -A80 -B15 'pub.*exotic_get_own_property|fn exotic_get_own_property|pub.*exotic_own_keys|fn exotic_own_keys' crates/perry-runtime/src/object/exotic_expando.rs crates/perry-runtime/src/object
printf '%s\n' '--- stringify entry and object assign entry ---'
rg -n -A35 -B20 'stringify_error_own_props|pub.*js_object_assign|fn js_object_assign|object_assign_set_string_key' crates/perry-runtime/src/json/stringify.rs crates/perry-runtime/src/object/alloc.rsRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact file locations ---'
find crates/perry-runtime/src -type f \( -name 'error.rs' -o -name 'exotic_expando.rs' \) -print
printf '%s\n' '--- error allocation implementation ---'
sed -n '1,220p' crates/perry-runtime/src/error.rs
printf '%s\n' '--- exotic implementation ---'
sed -n '1,280p' crates/perry-runtime/src/object/exotic_expando.rsRepository: PerryTS/perry Length of output: 21890 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- alloc_error remainder ---'
sed -n '180,300p' crates/perry-runtime/src/error.rs
printf '%s\n' '--- Error-side-table move handling ---'
rg -n -A35 -B12 'ERROR_USER_PROPS|error_user_props|user_prop.*moved|owner_moved|move.*error' crates/perry-runtime/src/node_submodules/diagnostics.rs crates/perry-runtime/src/gc crates/perry-runtime/src/object/exotic_expando.rs
printf '%s\n' '--- exotic operations definitions ---'
rg -n 'exotic_get_own_property|exotic_own_keys|invoke_accessor_getter|ACCESSOR_DESCRIPTORS' crates/perry-runtime/src/object/exotic_expando.rs
sed -n '280,560p' crates/perry-runtime/src/object/exotic_expando.rsRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exotic function line numbers ---'
rg -n 'exotic_get_own_property|exotic_own_keys|invoke_accessor_getter|ACCESSOR_DESCRIPTORS' crates/perry-runtime/src/object/exotic_expando.rs crates/perry-runtime/src/object
printf '%s\n' '--- exotic get/keys implementation ---'
sed -n '290,480p' crates/perry-runtime/src/object/exotic_expando.rs
printf '%s\n' '--- Error move hook registration and implementation ---'
rg -n -A25 -B15 'ErrorSideTables|error.*owner.*moved|rekey.*error|register.*error' crates/perry-runtime/src/gc crates/perry-runtime/src/node_submodules/diagnostics.rsRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Error GC move/finalize hooks ---'
find crates/perry-runtime/src -type f -name '*.rs' -print0 |
xargs -0 rg -n -A35 -B12 'ErrorSideTables|scan_error_user_props_roots_mut|error.*moved|ErrorSideTables.*move|GC_TYPE_ERROR.*GcMove'
printf '%s\n' '--- object assignment entry and source lifetime ---'
sed -n '1460,1585p' crates/perry-runtime/src/object/alloc.rs
printf '%s\n' '--- stringify call chain ---'
sed -n '120,230p' crates/perry-runtime/src/json/stringify.rs
rg -n -A30 -B15 'stringify_error_own_props|GC_TYPE_ERROR|ptr_is_native_error' crates/perry-runtime/src/json/stringify.rs
printf '%s\n' '--- accessor invocation contract ---'
sed -n '390,430p' crates/perry-runtime/src/object/field_get_set/accessors.rsRepository: PerryTS/perry Length of output: 50369 Root movable
Root each Error with 📍 Affects 3 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| pub(crate) unsafe fn build_fs_error_value( | ||
| err: &std::io::Error, | ||
| syscall: &'static str, | ||
|
|
@@ -112,13 +160,7 @@ pub(crate) unsafe fn build_fs_error_value( | |
| let msg = format!("{}: {}, {} '{}'", code, err, syscall, path); | ||
| let msg_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); | ||
| let err_ptr = crate::error::js_error_new_with_message(msg_ptr); | ||
| // Register code/syscall/path in the per-message side tables so the | ||
| // `.code`, `.syscall`, `.path` property getters in `field_get_set` | ||
| // surface Node-compatible values on caught errors. | ||
| crate::node_submodules::register_error_code_pub(msg_ptr, code); | ||
| crate::node_submodules::register_error_errno(msg_ptr, errno); | ||
| crate::node_submodules::register_error_syscall(msg_ptr, syscall); | ||
| crate::node_submodules::register_error_path(msg_ptr, path.to_string()); | ||
| attach_fs_error_props(err_ptr, code, errno, syscall, Some(path), None); | ||
| crate::value::js_nanbox_pointer(err_ptr as i64) | ||
| } | ||
|
|
||
|
|
@@ -136,11 +178,7 @@ pub(crate) unsafe fn build_fs_error_value_with_dest( | |
| let msg = format!("{}: {}, {} '{}' -> '{}'", code, err, syscall, path, dest); | ||
| let msg_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); | ||
| let err_ptr = crate::error::js_error_new_with_message(msg_ptr); | ||
| crate::node_submodules::register_error_code_pub(msg_ptr, code); | ||
| crate::node_submodules::register_error_errno(msg_ptr, errno); | ||
| crate::node_submodules::register_error_syscall(msg_ptr, syscall); | ||
| crate::node_submodules::register_error_path(msg_ptr, path.to_string()); | ||
| crate::node_submodules::register_error_dest(msg_ptr, dest.to_string()); | ||
| attach_fs_error_props(err_ptr, code, errno, syscall, Some(path), Some(dest)); | ||
| crate::value::js_nanbox_pointer(err_ptr as i64) | ||
| } | ||
|
|
||
|
|
@@ -153,9 +191,7 @@ pub(crate) unsafe fn build_fs_error_value_no_path( | |
| let msg = format!("{}: {}, {}", code, err, syscall); | ||
| let msg_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); | ||
| let err_ptr = crate::error::js_error_new_with_message(msg_ptr); | ||
| crate::node_submodules::register_error_code_pub(msg_ptr, code); | ||
| crate::node_submodules::register_error_errno(msg_ptr, errno); | ||
| crate::node_submodules::register_error_syscall(msg_ptr, syscall); | ||
| attach_fs_error_props(err_ptr, code, errno, syscall, None, None); | ||
| crate::value::js_nanbox_pointer(err_ptr as i64) | ||
| } | ||
|
|
||
|
|
||
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the documented enumeration order.
Line 19 says
code,errno,path,syscall. The implementation installs, and the fragment later documents,errno,code,syscall,path. Update the table so the Node comparison is consistent.Proposed fix
Based on learnings, changelog entries must ensure the entry detail matches the change type.
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Learnings