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
18 changes: 4 additions & 14 deletions crates/perry-codegen/src/expr/new_dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,27 +266,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}
}

// `new v8.GCProfiler()` (#3142) — represent the profiler instance
// as the `"v8.GCProfiler"` native-module namespace so its
// `start()` / `stop()` methods dispatch through the runtime
// native-module method table (same shape as `new crypto.Certificate`).
// `new v8.GCProfiler()` (#3142) — allocate a fresh native-module
// instance whose `start()` / `stop()` methods dispatch through the
// runtime native-module method table.
if let Expr::PropertyGet { object, property } = callee.as_ref() {
if property == "GCProfiler" {
if let Expr::NativeModuleRef(mod_name) = object.as_ref() {
if mod_name == "v8" {
for a in args {
let _ = lower_expr(ctx, a)?;
}
let module_name = "v8.GCProfiler";
let mod_idx = ctx.strings.intern(module_name);
let mod_bytes_global =
format!("@{}", ctx.strings.entry(mod_idx).bytes_global);
let mod_len_str = module_name.len().to_string();
return Ok(ctx.block().call(
DOUBLE,
"js_create_native_module_namespace",
&[(PTR, &mod_bytes_global), (I64, &mod_len_str)],
));
return Ok(ctx.block().call(DOUBLE, "js_v8_gc_profiler_new", &[]));
}
}
}
Expand Down
61 changes: 41 additions & 20 deletions crates/perry-codegen/src/lower_call/native/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,43 +224,46 @@ pub(crate) fn lower_native_method_call(
return Ok(v);
}

// node:v8 (#3137/#3138). serialize/deserialize + heap-stat helpers route to
// the `js_v8_*` runtime entry points. All are receiver-less statics.
// node:v8 (#3137/#3138/#3140). serialize/deserialize + heap-stat/snapshot
// helpers route to the `js_v8_*` runtime entry points. All are receiver-less
// statics.
if module == "v8" && object.is_none() {
let runtime = match method {
"serialize" => Some(("js_v8_serialize", true)),
"deserialize" => Some(("js_v8_deserialize", true)),
"getHeapStatistics" => Some(("js_v8_get_heap_statistics", false)),
"getHeapCodeStatistics" => Some(("js_v8_get_heap_code_statistics", false)),
"getHeapSpaceStatistics" => Some(("js_v8_get_heap_space_statistics", false)),
"cachedDataVersionTag" => Some(("js_v8_cached_data_version_tag", false)),
"serialize" => Some(("js_v8_serialize", 1usize)),
"deserialize" => Some(("js_v8_deserialize", 1)),
"getHeapStatistics" => Some(("js_v8_get_heap_statistics", 0)),
"getHeapCodeStatistics" => Some(("js_v8_get_heap_code_statistics", 0)),
"getHeapSpaceStatistics" => Some(("js_v8_get_heap_space_statistics", 0)),
"cachedDataVersionTag" => Some(("js_v8_cached_data_version_tag", 0)),
"getHeapSnapshot" => Some(("js_v8_get_heap_snapshot", 1)),
"writeHeapSnapshot" => Some(("js_v8_write_heap_snapshot", 2)),
// #3679: diagnostic-control / coverage helpers — Node-shaped no-op
// callables returning `undefined` (Perry has no V8 engine to drive
// real flag mutation or coverage capture). Args are evaluated for
// side effects then ignored.
"setFlagsFromString"
| "takeCoverage"
| "stopCoverage"
| "setHeapSnapshotNearHeapLimit" => Some(("js_v8_noop_undefined", false)),
| "setHeapSnapshotNearHeapLimit" => Some(("js_v8_noop_undefined", 0)),
_ => None,
};
if let Some((fname, takes_arg)) = runtime {
if takes_arg {
let arg = if let Some(first) = args.first() {
lower_expr(ctx, first)?
if let Some((fname, arity)) = runtime {
let mut lowered = Vec::with_capacity(arity);
for i in 0..arity {
let arg = if let Some(expr) = args.get(i) {
lower_expr(ctx, expr)?
} else {
double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
};
// Lower remaining args for side effects (Node ignores them).
for extra in args.iter().skip(1) {
let _ = lower_expr(ctx, extra)?;
}
return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &arg)]));
lowered.push(arg);
}
for extra in args {
// Lower remaining args for side effects (Node ignores them).
for extra in args.iter().skip(arity) {
let _ = lower_expr(ctx, extra)?;
}
return Ok(ctx.block().call(DOUBLE, fname, &[]));
let call_args: Vec<(crate::types::LlvmType, &str)> =
lowered.iter().map(|arg| (DOUBLE, arg.as_str())).collect();
return Ok(ctx.block().call(DOUBLE, fname, &call_args));
}
}

Expand Down Expand Up @@ -306,6 +309,24 @@ pub(crate) fn lower_native_method_call(
}
return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &arg)]));
}

// #3142: named-import GCProfiler instances lower their method calls to
// NativeMethodCall with `class_name == "GCProfiler"`. Route those to
// the same small runtime state machine as namespace-member calls.
if class_name == Some("GCProfiler") && matches!(method, "start" | "stop") {
if let Some(object) = object {
let recv = lower_expr(ctx, object)?;
for extra in args {
let _ = lower_expr(ctx, extra)?;
}
let fname = if method == "start" {
"js_v8_gc_profiler_start"
} else {
"js_v8_gc_profiler_stop"
};
return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &recv)]));
}
}
}

if module == "crypto"
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,11 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_v8_get_heap_code_statistics", DOUBLE, &[]);
module.declare_function("js_v8_get_heap_space_statistics", DOUBLE, &[]);
module.declare_function("js_v8_cached_data_version_tag", DOUBLE, &[]);
module.declare_function("js_v8_get_heap_snapshot", DOUBLE, &[DOUBLE]);
module.declare_function("js_v8_write_heap_snapshot", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_v8_gc_profiler_new", DOUBLE, &[]);
module.declare_function("js_v8_gc_profiler_start", DOUBLE, &[DOUBLE]);
module.declare_function("js_v8_gc_profiler_stop", DOUBLE, &[DOUBLE]);
module.declare_function("js_v8_gc_profiler_report", DOUBLE, &[]);
// node:v8 Serializer/Deserializer classes (#3680) + lifecycle/diagnostic (#3679).
module.declare_function("js_v8_serializer_new", DOUBLE, &[DOUBLE]);
Expand Down
11 changes: 7 additions & 4 deletions crates/perry-hir/src/lower/expr_call/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,9 +482,10 @@ pub(super) fn try_native_module_methods(
}
}

// node:v8 module methods (#3137/#3138). serialize/deserialize and
// the heap-stat helpers lower to a receiver-less NativeMethodCall
// dispatched in codegen to the `js_v8_*` runtime entry points.
// node:v8 module methods (#3137/#3138/#3140).
// serialize/deserialize, heap-stat helpers, and heap-snapshot
// helpers lower to a receiver-less NativeMethodCall dispatched in
// codegen to the `js_v8_*` runtime entry points.
let is_v8_module =
obj_name == "v8" || ctx.lookup_builtin_module_alias(&obj_name) == Some("v8");
if is_v8_module {
Expand All @@ -496,7 +497,9 @@ pub(super) fn try_native_module_methods(
| "getHeapStatistics"
| "getHeapCodeStatistics"
| "getHeapSpaceStatistics"
| "cachedDataVersionTag" => {
| "cachedDataVersionTag"
| "getHeapSnapshot"
| "writeHeapSnapshot" => {
return Ok(Ok(Expr::NativeMethodCall {
module: "v8".to_string(),
class_name: None,
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,20 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
});
}

if matches!(
ctx.lookup_native_module(&class_name),
Some(("v8", Some("GCProfiler")))
) {
let args = lower_optional_args(ctx, new_expr.args.as_deref())?;
return Ok(Expr::NewDynamic {
callee: Box::new(Expr::PropertyGet {
object: Box::new(Expr::NativeModuleRef("v8".to_string())),
property: "GCProfiler".to_string(),
}),
args,
});
}

if matches!(class_name.as_str(), "MIMEType" | "MIMEParams") {
if let Some((module_name, Some(method_name))) =
ctx.lookup_native_module(&class_name)
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/node_stream_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ pub extern "C" fn js_node_stream_readable_from_options(iterable: f64, opts: f64)
hidden_chunks_key(),
normalized.chunks,
);
initialize_readable_from_buffered_length(readable, normalized.chunks);
if let Some(source_iterator) = normalized.source_iterator {
js_object_set_field_by_name(
raw as *mut ObjectHeader,
Expand All @@ -730,6 +731,22 @@ pub extern "C" fn js_node_stream_readable_from_options(iterable: f64, opts: f64)
readable
}

fn initialize_readable_from_buffered_length(readable: f64, chunks: f64) {
let mut values = Vec::new();
push_chunk_values(chunks, &mut values, 0);
let length = if readable_object_mode(readable) {
values.len() as f64
} else {
let mut bytes = Vec::new();
for value in values {
append_chunk_bytes(value, &mut bytes, 0);
}
bytes.len() as f64
};
set_hidden_value(readable, hidden_buffered_key(), length);
set_hidden_value(readable, hidden_key(b"readableLength"), length);
}

// ─────────────────────────────────────────────────────────────────
// #1534: static introspection helpers `Readable.isDisturbed(s)` and
// `Readable.isErrored(s)`. Node returns booleans reflecting the
Expand Down
37 changes: 18 additions & 19 deletions crates/perry-runtime/src/node_stream_readwrite.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
//! node:stream — readable/writable state machine (flow control, pipes, read/write/transform impl) (split out of node_stream.rs for the 2000-line
//! file-size gate, #1987). Shares the parent module's constants, hidden-key
//! accessors and state primitives via `use super::*`.
//! node:stream readable/writable state, split from node_stream.rs for #1987.
#![allow(unused_imports)]
use super::*;
use crate::closure::{
Expand Down Expand Up @@ -240,6 +238,9 @@ pub(super) fn emit_readable_data(stream: f64, chunk: f64) {
}

pub(super) fn emit_readable_data_unchecked(stream: f64, chunk: f64) {
let Some(chunk) = super::decode_readable_chunk_for_encoding(stream, chunk) else {
return;
};
let _ = emit_stream_event(stream, string_value(b"data"), &[chunk]);
write_chunk_to_pipe_destinations(stream, chunk);
}
Expand Down Expand Up @@ -887,6 +888,18 @@ pub(super) fn read_stream_available_default(stream: f64) -> f64 {
schedule_readable_end(stream);
}
let encoded = readable_encoding_tag(stream).is_some();
if encoded {
let mut decoded = Vec::with_capacity(values.len());
for value in values {
if let Some(value) = super::decode_readable_chunk_for_encoding(stream, value) {
decoded.push(value);
}
}
values = decoded;
if values.is_empty() {
return f64::from_bits(TAG_NULL);
}
}
if values.len() == 1 {
if encoded {
return values[0];
Expand Down Expand Up @@ -1798,11 +1811,6 @@ pub(super) fn push_chunk_values(value: f64, out: &mut Vec<f64>, depth: u8) {
}

/// Drain the chunk storage Perry attaches in `Readable.from(iterable)`.
///
/// This intentionally handles only the current stream stub's concrete shapes:
/// arrays of strings/Buffers/Uint8Arrays/ArrayBuffers plus direct single
/// string/binary chunks. It gives `node:stream/consumers` useful data without
/// pretending Perry has a full Node stream pump yet.
pub fn js_node_stream_collect_bytes(stream: f64) -> Vec<u8> {
js_node_stream_collect_bytes_result(stream).unwrap_or_default()
}
Expand Down Expand Up @@ -1907,12 +1915,7 @@ pub(crate) fn js_node_stream_readable_chunks_result(stream: f64) -> Result<Optio
Ok(Some(out))
}

// ─────────────────────────────────────────────────────────────────
// Method tables. Order is locked in — it determines the shape's
// packed-keys order. Each method set's length is a unique
// shape-cache key when added to its base shape id, so the Readable,
// Writable, and Duplex method tables stay in distinct shape bands.
// ─────────────────────────────────────────────────────────────────
// Method table order determines packed-key order and shape-cache identity.

pub(super) fn readable_methods() -> [(&'static str, StubFn); 39] {
[
Expand Down Expand Up @@ -1940,11 +1943,7 @@ pub(super) fn readable_methods() -> [(&'static str, StubFn); 39] {
("destroy", cast1(ns_destroy1)),
("setEncoding", cast1(ns_set_encoding1)),
("isPaused", cast0(ns_is_paused0)),
// #1558 — async iterator helpers. The consuming helpers accept a
// trailing `{ signal }` options arg; the lazy transforms accept one
// too (Node's signature). Arities are registered in
// `register_iter_helper_arities` so under-supplied calls pad the
// missing trailing args with `undefined`.
// #1558: async iterator helpers; arities pad missing options args.
("toArray", cast1(ns_iter_to_array)),
("map", cast2(ns_iter_map)),
("filter", cast2(ns_iter_filter)),
Expand Down
19 changes: 19 additions & 0 deletions crates/perry-runtime/src/node_stream_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,25 @@ fn readable_from_retains_buffer_chunks_for_consumers() {
assert_eq!(js_node_stream_collect_bytes(readable), b"abcd");
}

#[test]
fn readable_from_set_encoding_read_decodes_buffer_chunks() {
let mut arr = crate::array::js_array_alloc(1);
arr = crate::array::js_array_push_f64(arr, buffer_value(b"{\"snapshot\":true}"));
let opts = crate::object::js_object_alloc(0, 1);
js_object_set_field_by_name(opts, hidden_key(b"objectMode"), f64::from_bits(TAG_FALSE));

let readable = js_node_stream_readable_from_options(
box_pointer(arr as *const u8),
box_pointer(opts as *const u8),
);
let handle = raw_ptr_from_value(readable) as i64;
js_node_stream_method_set_encoding(handle, string_value("utf8"));

let got = js_node_stream_method_read(handle, f64::from_bits(TAG_UNDEFINED));
assert!(JSValue::from_bits(got.to_bits()).is_any_string());
assert_eq!(string_contents(got), "{\"snapshot\":true}");
}

#[test]
fn readable_from_typed_uint8array_retains_numeric_byte_chunks() {
let mut arr = crate::array::js_array_alloc(3);
Expand Down
Loading