Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/gc-root-dominance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,13 +345,14 @@ jobs:
- name: Check root-store dominance (dependency-scale)
run: |
set -euo pipefail
# Floors from the corpus as of this commit (81 modules, ~12900
# functions, ~7700 root stores), set below that with room for the
# dependency's own churn. `zod` growing is fine; `zod` no longer
# compiling natively is the finding, and these are what make it one.
# The corpus retained 81 modules and ~14,000 root stores while
# compiler pruning reduced its function count from 12,909 to a
# stable 5,615-5,920. Keep the breadth floor below that measured
# population; module and root-store floors independently reject a
# compile that silently loses the dependency or the gate's subject.
python3 scripts/gc_root_dominance_check.py ir-corpus-dep \
--moving-only \
--min-files 60 --min-binds 4000 --min-funcs 6000 \
--min-files 60 --min-binds 4000 --min-funcs 5000 \
--allowlist scripts/gc_root_dominance_allowlist.json \
--seeded-violations 40 \
-v
Expand All @@ -362,7 +363,7 @@ jobs:
python3 scripts/gc_root_dominance_check.py ir-corpus-dep \
--unrooted-allocas \
--moving-only \
--min-files 60 --min-binds 4000 --min-funcs 6000 \
--min-files 60 --min-binds 4000 --min-funcs 5000 \
--allowlist scripts/gc_root_dominance_allowlist.json \
-v

Expand Down Expand Up @@ -401,7 +402,7 @@ jobs:
set -euo pipefail
python3 scripts/gc_root_dominance_check.py ir-corpus-dep \
--stale-registers --moving-only \
--min-files 60 --min-binds 4000 --min-funcs 6000 \
--min-files 60 --min-binds 4000 --min-funcs 5000 \
--max-stale 118

- name: Upload the IR corpus on failure
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/llvm-inprocess.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
cargo build --profile perry-dev -p perry -p perry-runtime-static \
-p perry-stdlib-static --features perry/llvm-inprocess

- name: Unit gates (528 incl. corpus construction + RS4GC pin)
- name: Unit gates (including corpus construction + RS4GC pin)
run: |
set -euo pipefail
out=$(cargo test --profile perry-dev -p perry-codegen \
Expand All @@ -136,7 +136,9 @@ jobs:
grep -q "dialect::tests::corpus_spike ... ok" <<<"$out"
grep -q "dialect::tests::corpus_batch_kernel ... ok" <<<"$out"
grep -q "dialect::tests::corpus_exception_handling ... ok" <<<"$out"
grep -q "inprocess::tests::rs4gc_schedules_in_process ... ok" <<<"$out"
# The implementation may move between inprocess submodules; the
# unique test name is the stable contract that proves this pin ran.
grep -Eq '^test .*::rs4gc_schedules_in_process \.\.\. ok$' <<<"$out"


# The tracked `.ll` corpora above are a SNAPSHOT of what the compiler
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,17 @@ jobs:
python3 scripts/check_llvm_corpus_currency.py --self-test
python3 scripts/check_llvm_corpus_currency.py

# #9925. Both native root-dominance corpus generators scraped the
# production statepoint pass constant from inprocess.rs, so splitting
# that module left the scheduled gate unable to emit either corpus. The
# shared reader follows the unique declaration across codegen modules;
# keep its parser and the live repository lookup in required lint.
- name: Statepoint pass source reader
if: ${{ !cancelled() }}
run: |
python3 scripts/read_statepoint_rewrite_passes.py --self-test
python3 scripts/read_statepoint_rewrite_passes.py --check

# #7877, second round. The knob audit above covers env-var names; it says
# nothing about the paths and numbers the same pages state. Both drifted:
# the memory-model source map pointed at a `gc.rs` deleted in the module
Expand Down
5 changes: 5 additions & 0 deletions changelog.d/9924-gc-promotion-rollback-young-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Preserve side-table young-root logs when the first copying collection
abandons speculative in-place promotion, so the evacuation retry does not
lose live objects or corrupt full applications during startup.
5 changes: 5 additions & 0 deletions changelog.d/9926-child-process-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Match Node's `child_process` metadata and synchronous buffer-limit behavior by
preserving the caller's `execFile` spelling and signaling a process when its
output crosses `maxBuffer` only while it is still running.
7 changes: 7 additions & 0 deletions changelog.d/9931-class-expression-typeof-self.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Fixed

- Resolve a named class expression's inner name inside `typeof`, including
when its outer binding has a different name and the compiler registers the
class under a generated key. The unresolved-global shortcut now excludes the
active class inner name so lexical lowering can resolve the class binding;
an HIR regression covers that path.
5 changes: 5 additions & 0 deletions changelog.d/9932-typed-array-own-property-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Preserve property creation order when enumerating ordinary properties on
buffers, data views, and buffer-backed typed arrays. Updating a property now
keeps its position, while deleting and recreating it appends the key.
5 changes: 5 additions & 0 deletions changelog.d/9933-util-inspect-lazy-error-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Materialize lazy Error stacks when `util.inspect()` formats an Error with a
`cause` or an `AggregateError` with `errors`, preserving Node-compatible
stack/body layout.
4 changes: 4 additions & 0 deletions changelog.d/9934-module-builtin-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Keep Perry's `node:ffi` extension resolvable without exposing it through
Node's `module.builtinModules` and `Module.isBuiltin()` inventory.
9 changes: 9 additions & 0 deletions crates/perry-hir/src/lower/lower_expr/arm_unary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ pub(crate) fn lower_unary_expr(ctx: &mut LoweringContext, unary: &ast::UnaryExpr
&& ctx.lookup_native_module(n).is_none()
&& ctx.lookup_imported_func(n).is_none()
&& ctx.lookup_class(n).is_none()
// A named class expression can use a synthetic registry key
// while its source-level inner binding keeps the written
// name. Let ordinary identifier lowering resolve that binding
// to the current class instead of treating it as an optional
// global. For example, `var B = class l { static f() {
// return typeof l } }` registers the class under a generated
// `l__class_expr_*` key, but `l` is still lexically bound in
// the class body.
&& ctx.current_class_inner_name.as_deref() != Some(n)
&& !is_builtin_function(n)
&& !is_known_global_identifier_name(n)
&& !matches!(n, "undefined" | "null" | "NaN" | "Infinity")
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,33 @@ fn named_class_expr_static_private_update_in_arrow_keeps_lexical_brand_owner() {
);
}

/// A named class expression whose outer binding has a different name uses a
/// synthetic registry key. `typeof` must still resolve the source-level inner
/// name through the class body's lexical binding rather than an optional
/// global lookup.
#[test]
fn typeof_named_class_expr_inner_binding_uses_the_current_class() {
let source = r#"
var B = class l {
static selfType(): string { return typeof l; }
};
"#;
let module = perry_parser::parse_typescript(source, "t.ts").expect("source parses");
let hir = super::lower_module(&module, "t", "t.ts").expect("source lowers");
let method = hir
.classes
.iter()
.flat_map(|class| &class.static_methods)
.find(|method| method.name == "selfType")
.expect("static selfType method is lowered");
let body = format!("{:#?}", method.body);

assert!(
body.contains("ClassRef") && !body.contains("js_global_get_optional"),
"the class's inner name must resolve to its synthetic ClassRef: {body}"
);
}

/// A sibling class declaration is already a known lexical binding while an
/// earlier class method is lowered, even though its registry entry is emitted
/// later. The unresolved-constructor guard must preserve that forward binding.
Expand Down
74 changes: 58 additions & 16 deletions crates/perry-runtime/src/buffer/own_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};

type BufferProps = HashMap<usize, HashMap<String, u64>>;
#[derive(Default)]
struct BufferOwnProps {
values: HashMap<String, u64>,
order: Vec<String>,
}

type BufferProps = HashMap<usize, BufferOwnProps>;

fn buffer_props() -> &'static Mutex<BufferProps> {
static PROPS: OnceLock<Mutex<BufferProps>> = OnceLock::new();
Expand Down Expand Up @@ -93,10 +99,11 @@ pub fn buffer_define_own_data_prop(addr: usize, prop: &str, value: f64) {
}
BUFFER_OWN_PROPS_EVER.store(true, Ordering::Release);
if let Ok(mut props) = buffer_props().lock() {
props
.entry(addr)
.or_default()
.insert(prop.to_string(), value.to_bits());
let own = props.entry(addr).or_default();
if !own.values.contains_key(prop) {
own.order.push(prop.to_string());
}
own.values.insert(prop.to_string(), value.to_bits());
}
}

Expand All @@ -108,7 +115,12 @@ pub fn buffer_get_own_prop(addr: usize, prop: &str) -> Option<f64> {
buffer_props()
.lock()
.ok()
.and_then(|props| props.get(&addr).and_then(|m| m.get(prop)).copied())
.and_then(|props| {
props
.get(&addr)
.and_then(|own| own.values.get(prop))
.copied()
})
.map(f64::from_bits)
}

Expand Down Expand Up @@ -140,8 +152,7 @@ pub fn buffer_read_own_prop(addr: usize, prop: &str) -> Option<f64> {
buffer_get_own_prop(addr, prop)
}

/// Every own dynamic prop key recorded for `addr`, in insertion-independent
/// (sorted) order.
/// Every own dynamic prop key recorded for `addr`, in property-creation order.
///
/// #8149: `Object.keys` / `getOwnPropertyNames` / `for…in` need these. Before,
/// the enumeration paths had no registered-buffer arm at all and walked a
Expand All @@ -156,13 +167,11 @@ pub fn buffer_own_prop_names(addr: usize) -> Vec<String> {
if addr == 0 || !buffer_own_props_possible() {
return Vec::new();
}
let mut names: Vec<String> = buffer_props()
buffer_props()
.lock()
.ok()
.and_then(|props| props.get(&addr).map(|m| m.keys().cloned().collect()))
.unwrap_or_default();
names.sort();
names
.and_then(|props| props.get(&addr).map(|own| own.order.clone()))
.unwrap_or_default()
}

/// Whether the buffer carries any own dynamic prop under `prop`.
Expand All @@ -183,10 +192,13 @@ pub fn buffer_delete_own_prop(addr: usize, prop: &str) -> bool {
let Some(entries) = props.get_mut(&addr) else {
return false;
};
let removed = entries.remove(prop).is_some();
let removed = entries.values.remove(prop).is_some();
if removed {
entries.order.retain(|key| key != prop);
}
crate::object::clear_accessor_descriptor(addr, prop);
crate::object::clear_property_attrs(addr, prop);
if entries.is_empty() {
if entries.values.is_empty() {
props.remove(&addr);
}
removed
Expand All @@ -210,7 +222,7 @@ pub fn scan_buffer_own_props_roots_mut(visitor: &mut crate::gc::RuntimeRootVisit
};
let mut new_owner = owner;
visitor.visit_metadata_usize_slot(&mut new_owner);
for bits in entries.values_mut() {
for bits in entries.values.values_mut() {
let mut v = f64::from_bits(*bits);
visitor.visit_nanbox_f64_slot(&mut v);
*bits = v.to_bits();
Expand Down Expand Up @@ -253,3 +265,33 @@ pub fn clear_buffer_own_props(addr: usize) {
props.remove(&addr);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn own_property_names_preserve_creation_order() {
let owner_marker = Box::new(0_u8);
let owner = (&*owner_marker as *const u8) as usize;

clear_buffer_own_props(owner);
buffer_define_own_data_prop(owner, "second", 2.0);
buffer_define_own_data_prop(owner, "first", 1.0);
buffer_define_own_data_prop(owner, "second", 22.0);
assert_eq!(
buffer_own_prop_names(owner),
["second", "first"],
"updating a property must keep its original position"
);

assert!(buffer_delete_own_prop(owner, "second"));
buffer_define_own_data_prop(owner, "second", 222.0);
assert_eq!(
buffer_own_prop_names(owner),
["first", "second"],
"deleting and recreating a property must append it"
);
clear_buffer_own_props(owner);
}
}
8 changes: 7 additions & 1 deletion crates/perry-runtime/src/builtins/formatting/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ unsafe fn format_error_headline(error_ptr: *const crate::error::ErrorHeader) ->
}

unsafe fn format_error_stack_frame(error_ptr: *const crate::error::ErrorHeader) -> Option<String> {
let stack = string_header_to_string((*error_ptr).stack, "");
// #9486 made Error stacks lazy: `ErrorHeader.stack` stays null until the
// first observable read materializes the captured frame payload. Inspect
// is one of those reads. Looking at the slot directly made Errors with a
// cause or AggregateError.errors print a standalone `{` where Node keeps
// it attached to the final stack line.
let stack_ptr = crate::error::materialize_error_stack(error_ptr.cast_mut());
let stack = string_header_to_string(stack_ptr, "");
stack
.lines()
.skip(1)
Expand Down
28 changes: 20 additions & 8 deletions crates/perry-runtime/src/child_process/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ pub extern "C" fn js_child_process_exec_sync(
};
cp_apply_options(&mut command, opts_val);

let mut run_options = cp_read_sync_stdio_run_options(opts_val);
run_options.mark_shell_command();
let run_options = cp_read_sync_stdio_run_options(opts_val);
let run = cp_run_to_completion(command, &run_options);
let stdout_box = cp_box_run_output(&run.stdout, run.stdout_piped, &mode);
if run.success() {
Expand Down Expand Up @@ -298,8 +297,7 @@ pub extern "C" fn js_child_process_exec(cmd_ptr: *const StringHeader, arg1: f64,
c
};
cp_apply_options(&mut command, arg1);
let mut run_options = cp_read_async_run_options(arg1);
run_options.mark_shell_command();
let run_options = cp_read_async_run_options(arg1);

if cb.is_null() {
// Legacy no-callback shape — run synchronously and return stdout
Expand All @@ -312,7 +310,7 @@ pub extern "C" fn js_child_process_exec(cmd_ptr: *const StringHeader, arg1: f64,

// With a callback, run asynchronously: off the main thread, with the
// callback fired on a later event-loop tick (#4912).
reactor::cp_exec_async(command, cmd_str, cb_val, run_options, mode)
reactor::cp_exec_async(command, cmd_str, None, cb_val, run_options, mode)
}

/// `child_process.execFile(file[, args][, options][, callback])` — like `exec`
Expand Down Expand Up @@ -383,6 +381,7 @@ pub extern "C" fn js_child_process_exec_file(
reactor::cp_exec_async(
command,
cp_file_cmd_display(&file_str, &arg_strs),
Some(file_str),
cb_nanbox,
run_options,
mode,
Expand Down Expand Up @@ -471,7 +470,12 @@ extern "C" fn cp_promise_settle_cb(
/// `command` through the async exec reactor (#4912). Returns the NaN-boxed
/// pending promise. The settle closure (and through it the promise) is kept
/// alive by the reactor's exec-callback GC root.
fn cp_promisified_run(command: Command, cmd_str: String, opts: f64) -> f64 {
fn cp_promisified_run(
command: Command,
cmd_str: String,
public_spawnfile: Option<String>,
opts: f64,
) -> f64 {
let run_options = cp_read_async_run_options(opts);
// promisify(exec)/promisify(execFile) yield string stdout/stderr (utf8).
let mode = cp_read_output_mode(opts, true);
Expand All @@ -480,7 +484,14 @@ fn cp_promisified_run(command: Command, cmd_str: String, opts: f64) -> f64 {
let cb = js_closure_alloc(cp_promise_settle_cb as *const u8, 1);
js_closure_set_capture_ptr(cb, 0, cp_box_ptr(promise as *const u8).to_bits() as i64);
let cb_val = crate::value::js_nanbox_pointer(cb as i64);
let child = reactor::cp_exec_async(command, cmd_str, cb_val, run_options, mode);
let child = reactor::cp_exec_async(
command,
cmd_str,
public_spawnfile,
cb_val,
run_options,
mode,
);
crate::object::exotic_expando::value_store(
crate::object::exotic_expando::ExoticKind::Promise,
promise as usize,
Expand Down Expand Up @@ -509,7 +520,7 @@ extern "C" fn cp_promisified_exec(_closure: *const ClosureHeader, cmd_val: f64,
c
};
cp_apply_options(&mut command, opts);
cp_promisified_run(command, cmd, opts)
cp_promisified_run(command, cmd, None, opts)
}

extern "C" fn cp_promisified_exec_file(
Expand All @@ -529,6 +540,7 @@ extern "C" fn cp_promisified_exec_file(
cp_promisified_run(
command,
cp_file_cmd_display(&file, &arg_strs),
Some(file),
f64::from_bits(TAG_UNDEFINED_BITS),
)
}
Expand Down
Loading
Loading