diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 8ffdb8c82c..4075906eb0 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -907,6 +907,7 @@ pub(super) fn compile_closure( native_arena_owner_aliases: HashMap::new(), native_arena_ambiguous_owner_aliases: HashSet::new(), disable_buffer_fast_path: cross_module.disable_buffer_fast_path, + program_shadows_buffer_read_method: cross_module.program_shadows_buffer_read_method, min_length_bounds: HashMap::new(), bounded_buffer_index_pairs: Vec::new(), guarded_buffer_index_pairs: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 88460db72c..d7140d9b66 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -731,6 +731,7 @@ pub(super) fn compile_module_entry( native_arena_owner_aliases: HashMap::new(), native_arena_ambiguous_owner_aliases: HashSet::new(), disable_buffer_fast_path: cross_module.disable_buffer_fast_path, + program_shadows_buffer_read_method: cross_module.program_shadows_buffer_read_method, min_length_bounds: HashMap::new(), bounded_buffer_index_pairs: Vec::new(), guarded_buffer_index_pairs: Vec::new(), @@ -1298,6 +1299,7 @@ pub(super) fn compile_module_entry( native_arena_owner_aliases: HashMap::new(), native_arena_ambiguous_owner_aliases: HashSet::new(), disable_buffer_fast_path: cross_module.disable_buffer_fast_path, + program_shadows_buffer_read_method: cross_module.program_shadows_buffer_read_method, min_length_bounds: HashMap::new(), bounded_buffer_index_pairs: Vec::new(), guarded_buffer_index_pairs: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 95a083f101..f430e49775 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -614,6 +614,7 @@ pub(super) fn compile_function( native_arena_owner_aliases: HashMap::new(), native_arena_ambiguous_owner_aliases: HashSet::new(), disable_buffer_fast_path: cross_module.disable_buffer_fast_path, + program_shadows_buffer_read_method: cross_module.program_shadows_buffer_read_method, min_length_bounds: HashMap::new(), bounded_buffer_index_pairs: Vec::new(), guarded_buffer_index_pairs: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 2f3e83d9fd..ef56e68a6c 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -532,6 +532,7 @@ pub(super) fn compile_method( native_arena_owner_aliases: HashMap::new(), native_arena_ambiguous_owner_aliases: HashSet::new(), disable_buffer_fast_path: cross_module.disable_buffer_fast_path, + program_shadows_buffer_read_method: cross_module.program_shadows_buffer_read_method, min_length_bounds: HashMap::new(), bounded_buffer_index_pairs: Vec::new(), guarded_buffer_index_pairs: Vec::new(), @@ -1526,6 +1527,7 @@ pub(super) fn compile_static_method( native_arena_owner_aliases: HashMap::new(), native_arena_ambiguous_owner_aliases: HashSet::new(), disable_buffer_fast_path: cross_module.disable_buffer_fast_path, + program_shadows_buffer_read_method: cross_module.program_shadows_buffer_read_method, min_length_bounds: HashMap::new(), bounded_buffer_index_pairs: Vec::new(), guarded_buffer_index_pairs: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 4ff2ecae62..4a58f6eb93 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -1578,6 +1578,8 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> compiler_private_async_i32_control_locals, compiler_private_async_i1_control_locals, disable_buffer_fast_path, + program_shadows_buffer_read_method: + crate::lower_call::buffer_intrinsic::module_shadows_buffer_read_method(hir), flat_const_arrays: { // Issue #50: fold module-level `const X: number[][] = [[int, ...], ...]` // into a flat `[N x i32]` LLVM constant so `X[i][j]` / `krow[j]` can diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index 6c34fbb31b..57c469c280 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -812,6 +812,10 @@ pub(crate) struct CrossModuleCtx { /// Debug/benchmark switch that forces Buffer/Uint8Array accesses through /// the generic helper path. pub disable_buffer_fast_path: bool, + /// #6405: set when this module assigns a Buffer numeric read-method name as + /// an own property (`buf.readUInt8 = fn`). Threaded into every FnCtx so the + /// inline read intrinsic deopts to own-prop-aware runtime dispatch. + pub program_shadows_buffer_read_method: bool, /// (Issue #50) Module-level `const` 2D int arrays folded into flat /// `[N x i32]` LLVM constants. Maps local_id → info. Populated by /// scanning `hir.init`; threaded through every FnCtx so the IndexGet diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index f1b1f6f0c3..17439d68a5 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -997,6 +997,12 @@ pub(crate) struct FnCtx<'a> { /// Benchmark/debug switch that forces tracked buffers through the existing /// helper fallback instead of native GEP/load/store lowering. pub disable_buffer_fast_path: bool, + /// #6405: this module assigns a Buffer numeric read-method name as an own + /// property somewhere (`buf.readUInt8 = fn`), so an own prop may shadow the + /// prototype method. When set, `try_emit_buffer_read_intrinsic` deopts the + /// inline byte-load fold to the own-prop-aware runtime dispatch. False for + /// every program that never shadows a Buffer method (the common case). + pub program_shadows_buffer_read_method: bool, /// LocalId facts of the form `n = min(src.length, dst.length)`. pub min_length_bounds: std::collections::HashMap>, /// Loop-local facts proving a buffer index is bounded inside the current diff --git a/crates/perry-codegen/src/lower_call/buffer_intrinsic.rs b/crates/perry-codegen/src/lower_call/buffer_intrinsic.rs index 5782fa8701..4c9d8c4303 100644 --- a/crates/perry-codegen/src/lower_call/buffer_intrinsic.rs +++ b/crates/perry-codegen/src/lower_call/buffer_intrinsic.rs @@ -117,12 +117,243 @@ fn classify_buffer_numeric_read(method: &str) -> Option { }) } +/// True for a Buffer numeric READ accessor name (`readUInt8`, `readInt32BE`, +/// …) — the method family `try_emit_buffer_read_intrinsic` inline-folds. +/// Shared with the whole-module shadow scan so the fold table and the deopt +/// decision stay in lockstep (one source of truth: `classify_buffer_numeric_read`). +pub(crate) fn is_buffer_numeric_read_method(name: &str) -> bool { + classify_buffer_numeric_read(name).is_some() +} + +/// Issue #6405 — whole-module pre-codegen scan: does this module assign to a +/// property whose name is a Buffer numeric read-method (`buf.readUInt8 = fn`, +/// `buf["readInt32BE"] = fn`)? +/// +/// Node's Buffer IS an ordinary `Uint8Array`, so an own property SHADOWS the +/// same-named prototype method. Every dynamic dispatch path already honors +/// this (`dispatch_buffer_method` checks own props first), but a statically +/// provable `buf.readUInt8(0)` folds to the inline byte-load intrinsic below, +/// which reads the bytes directly and never consults the property table — so +/// the override was ignored. When any such assignment exists, the intrinsic +/// bails (returns `Ok(None)`) so the call routes through `js_native_call_method` +/// and the own-prop shadow wins. Zero runtime cost for the overwhelmingly +/// common program that never shadows a Buffer method — the fast path is +/// untouched there. +/// +/// A per-module scan is sufficient: the intrinsic only fires on a buffer local +/// that `lower_buffer_access_proof` proves non-escaping (a closure-captured or +/// cross-module-shared/exported buffer is stamped hazardous and never folds), +/// so any shadow that can reach a folded read lives in this same module. Only +/// literal property names are matched; a dynamic `buf[computedName] = fn` is +/// out of scope (that shape already defeats the static buffer proof in +/// practice — see the issue). +pub(crate) fn module_shadows_buffer_read_method(module: &perry_hir::Module) -> bool { + use perry_hir::{Expr, Stmt}; + + fn expr_shadows(expr: &Expr, found: &mut bool) { + if *found { + return; + } + match expr { + Expr::PropertySet { property, .. } if is_buffer_numeric_read_method(property) => { + *found = true; + return; + } + Expr::PutValueSet { key, .. } => { + if let Expr::String(name) = key.as_ref() { + if is_buffer_numeric_read_method(name) { + *found = true; + return; + } + } + } + _ => {} + } + perry_hir::walker::walk_expr_children(expr, &mut |child| expr_shadows(child, found)); + } + + // Exhaustive on `Stmt` on purpose (no catch-all) — a new statement variant + // that carries expressions must be threaded here, mirroring the walker's + // enforced-exhaustiveness contract, or a shadow inside it slips through. + fn stmt_shadows(stmt: &Stmt, found: &mut bool) { + if *found { + return; + } + match stmt { + Stmt::Expr(e) | Stmt::Throw(e) => expr_shadows(e, found), + Stmt::Return(opt) => { + if let Some(e) = opt { + expr_shadows(e, found); + } + } + Stmt::Let { init, .. } => { + if let Some(e) = init { + expr_shadows(e, found); + } + } + Stmt::Labeled { body, .. } => stmt_shadows(body, found), + Stmt::If { + condition, + then_branch, + else_branch, + } => { + expr_shadows(condition, found); + for s in then_branch { + stmt_shadows(s, found); + } + if let Some(eb) = else_branch { + for s in eb { + stmt_shadows(s, found); + } + } + } + Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { + expr_shadows(condition, found); + for s in body { + stmt_shadows(s, found); + } + } + Stmt::For { + init, + condition, + update, + body, + } => { + if let Some(i) = init { + stmt_shadows(i, found); + } + if let Some(c) = condition { + expr_shadows(c, found); + } + if let Some(u) = update { + expr_shadows(u, found); + } + for s in body { + stmt_shadows(s, found); + } + } + Stmt::Try { + body, + catch, + finally, + } => { + for s in body { + stmt_shadows(s, found); + } + if let Some(catch_clause) = catch { + for s in &catch_clause.body { + stmt_shadows(s, found); + } + } + if let Some(finally_b) = finally { + for s in finally_b { + stmt_shadows(s, found); + } + } + } + Stmt::Switch { + discriminant, + cases, + } => { + expr_shadows(discriminant, found); + for case in cases { + if let Some(test) = &case.test { + expr_shadows(test, found); + } + for s in &case.body { + stmt_shadows(s, found); + } + } + } + Stmt::Break + | Stmt::Continue + | Stmt::LabeledBreak(_) + | Stmt::LabeledContinue(_) + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) => {} + } + } + + fn scan_body(body: &[Stmt], found: &mut bool) { + for s in body { + stmt_shadows(s, found); + if *found { + return; + } + } + } + + let mut found = false; + // Top-level init, every function (which — post closure-conversion — also + // carries the module's nested closures and object-literal methods), and + // every class member body. + scan_body(&module.init, &mut found); + for func in &module.functions { + if found { + return true; + } + scan_body(&func.body, &mut found); + } + for class in &module.classes { + if found { + return true; + } + // A shadow assignment can hide in any expression position of a class, + // not just member bodies: a dynamic `extends` expression, a field + // initializer or computed field key, or a computed member key. Field + // initializers in particular live in `fields[*].init` (emitted via + // `apply_field_initializers_recursive`), NOT the constructor body, so + // they need an explicit walk. (We do NOT match member *names* — a class + // method/field named `readUInt8` is a user-class member, never an own + // property on a `Buffer.alloc` local, so it can't shadow a folded read.) + if let Some(ext) = &class.extends_expr { + expr_shadows(ext, &mut found); + } + for f in class.fields.iter().chain(class.static_fields.iter()) { + if let Some(key) = &f.key_expr { + expr_shadows(key, &mut found); + } + if let Some(init) = &f.init { + expr_shadows(init, &mut found); + } + } + if let Some(ctor) = &class.constructor { + scan_body(&ctor.body, &mut found); + } + for m in &class.methods { + scan_body(&m.body, &mut found); + } + for m in &class.static_methods { + scan_body(&m.body, &mut found); + } + for (_, g) in &class.getters { + scan_body(&g.body, &mut found); + } + for (_, s) in &class.setters { + scan_body(&s.body, &mut found); + } + for cm in &class.computed_members { + expr_shadows(&cm.key_expr, &mut found); + scan_body(&cm.function.body, &mut found); + } + } + found +} + pub(super) fn try_emit_buffer_read_intrinsic( ctx: &mut FnCtx<'_>, object: &Expr, method: &str, args: &[Expr], ) -> Result> { + // #6405: an own property shadows the same-named Buffer.prototype method. + // If the module assigns any such method name as a property, deopt the + // inline read fold so the call routes through the own-prop-aware runtime + // dispatch. The flag is module-wide but only set for programs that + // actually shadow, so the fast path is unaffected everywhere else. + if ctx.program_shadows_buffer_read_method { + return Ok(None); + } let spec = match classify_buffer_numeric_read(method) { Some(s) => s, None => return Ok(None), @@ -284,3 +515,138 @@ fn target_endian() -> BufferEndian { BufferEndian::Little } } + +#[cfg(test)] +mod shadow_scan_tests { + use super::module_shadows_buffer_read_method; + use perry_hir::{Class, ClassField, Expr, Module, Stmt}; + + fn filler() -> Box { + Box::new(Expr::Integer(0)) + } + + /// `x[key] = v` (computed set with an explicit receiver — how + /// `(b as any).readUInt8 = fn` and `b["readUInt8"] = fn` both lower). + fn put_value_set_expr(key: &str) -> Expr { + Expr::PutValueSet { + target: filler(), + key: Box::new(Expr::String(key.to_string())), + value: filler(), + receiver: filler(), + strict: false, + } + } + + fn put_value_set(key: &str) -> Stmt { + Stmt::Expr(put_value_set_expr(key)) + } + + /// A bare class carrying only the given instance fields — every other slot + /// empty. Used to prove the scan walks field *initializers*, not just member + /// bodies (field inits live in `fields[*].init`, emitted separately from the + /// constructor body). + fn class_with_fields(fields: Vec) -> Class { + Class { + id: 1, + name: "C".to_string(), + type_params: Vec::new(), + extends: None, + extends_name: None, + native_extends: None, + extends_expr: None, + heritage_lexically_shadowed: false, + fields, + constructor: None, + methods: Vec::new(), + getters: Vec::new(), + setters: Vec::new(), + static_accessor_names: Vec::new(), + static_accessor_fn_ids: Vec::new(), + static_fields: Vec::new(), + static_methods: Vec::new(), + computed_members: Vec::new(), + decorators: Vec::new(), + is_exported: false, + aliases: Vec::new(), + is_nested: false, + } + } + + fn field_with_init(name: &str, init: Expr) -> ClassField { + ClassField { + name: name.to_string(), + key_expr: None, + ty: perry_types::Type::Any, + init: Some(init), + is_private: false, + is_readonly: false, + decorators: Vec::new(), + } + } + + /// `x.prop = v` (dot set). + fn property_set(prop: &str) -> Stmt { + Stmt::Expr(Expr::PropertySet { + object: filler(), + property: prop.to_string(), + value: filler(), + }) + } + + #[test] + fn detects_read_method_shadow_in_init() { + let mut m = Module::new("t"); + m.init = vec![put_value_set("readUInt8")]; + assert!(module_shadows_buffer_read_method(&m)); + } + + #[test] + fn detects_dot_shadow_nested_in_control_flow() { + let mut m = Module::new("t"); + m.init = vec![Stmt::If { + condition: Expr::Bool(true), + then_branch: vec![property_set("readInt32BE")], + else_branch: None, + }]; + assert!(module_shadows_buffer_read_method(&m)); + } + + #[test] + fn ignores_non_read_method_names() { + let mut m = Module::new("t"); + // `writeUInt8` is a WRITE method (no read-intrinsic fold to protect), + // and `foo` is an ordinary expando — neither can shadow a folded read. + m.init = vec![put_value_set("writeUInt8"), property_set("foo")]; + assert!(!module_shadows_buffer_read_method(&m)); + } + + #[test] + fn empty_module_does_not_shadow() { + assert!(!module_shadows_buffer_read_method(&Module::new("t"))); + } + + #[test] + fn detects_shadow_in_class_field_initializer() { + // `class C { tag = ((buf as any).readDoubleLE = fn); }` — the shadow + // lives in `fields[*].init`, not the constructor body (#6405 review). + let mut m = Module::new("t"); + m.classes.push(class_with_fields(vec![field_with_init( + "tag", + put_value_set_expr("readDoubleLE"), + )])); + assert!(module_shadows_buffer_read_method(&m)); + } + + #[test] + fn plain_class_field_does_not_shadow() { + // A field NAMED like a read method (`class C { readUInt8 = 0; }`) is a + // user-class member, not an own-prop assignment on a Buffer — it must + // NOT trip the scan. + let mut m = Module::new("t"); + m.classes.push(class_with_fields(vec![field_with_init( + "readUInt8", + Expr::Integer(0), + )])); + assert!(!module_shadows_buffer_read_method(&m)); + } +} diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index 53ac3f64e4..808b83c0a1 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -32,7 +32,7 @@ use crate::expr::{variant_name, FnCtx}; // extraction of the original `lower_call.rs`'s 4.3k-LOC body so // every file in this directory stays under 2000 lines. mod atomics; -mod buffer_intrinsic; +pub(crate) mod buffer_intrinsic; mod builtin; mod builtin_table_gate; mod capture_writeback; diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index 81788776bb..33c9ef3f8d 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -28,6 +28,37 @@ fn finite_nonnegative_u32_index(index: f64) -> Option { } } +/// A canonical non-negative integer array-index string ("0", "2", "10", …) — +/// how a `Buffer`/`Uint8Array` `[[Get]]` treats a STRING key: it reads the byte +/// at that index rather than a named property (`buf["2"]` === `buf[2]`). +/// Leading-zero forms (`"01"`), signs, fractions, and values past `i32::MAX` +/// are ordinary property names, not indices. Reads the `StringHeader` bytes +/// directly (valid for heap and materialized short strings alike). +unsafe fn canonical_buffer_index(key_ptr: *const crate::StringHeader) -> Option { + if key_ptr.is_null() { + return None; + } + let len = (*key_ptr).byte_len as usize; + if len == 0 || len > 10 { + return None; + } + let bytes = std::slice::from_raw_parts( + (key_ptr as *const u8).add(std::mem::size_of::()), + len, + ); + if bytes[0] == b'0' && len > 1 { + return None; + } + let mut val: u64 = 0; + for &b in bytes { + if !b.is_ascii_digit() { + return None; + } + val = val * 10 + u64::from(b - b'0'); + } + (val <= i32::MAX as u64).then_some(val as u32) +} + /// Tag-aware dynamic index dispatch for `obj[key]` where `obj` has unknown /// static type. Issue #514. Strings → js_string_char_at; objects stringify /// numeric keys (`obj[0]` is `obj["0"]`), while arrays/buffers keep numeric @@ -158,16 +189,45 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { ); } if crate::buffer::is_registered_buffer(raw_ptr) { - let Some(idx_i32) = finite_nonnegative_i32_index(index) else { - return f64::from_bits(TAG_UNDEFINED); - }; let buf = raw_ptr as *const crate::buffer::BufferHeader; - let len = unsafe { (*buf).length }; - if (idx_i32 as u32) >= len { - return f64::from_bits(TAG_UNDEFINED); + if let Some(idx_i32) = finite_nonnegative_i32_index(index) { + let len = unsafe { (*buf).length }; + if (idx_i32 as u32) >= len { + return f64::from_bits(TAG_UNDEFINED); + } + let byte_val = crate::buffer::js_buffer_get(buf, idx_i32); + return byte_val as f64; + } + // A non-numeric (string) key: Node's Buffer is an ordinary Uint8Array + // object, so `buf[k]` with a string-valued `k` reads an OWN property + // (else the shadowed prototype method) — NOT a byte. This arm used to + // return `undefined`, so `(buf as any)[k] = v; (buf as any)[k]` — with + // `k` statically `any` but a string at runtime — read back `undefined` + // even though the write stored the own prop via + // `js_object_set_index_polymorphic` → `buffer_set_own_prop` (#6412). + // Route through the by-name getter, which resolves buffer own props + + // bound method values (`buffer_own_prop_or_method`), matching the + // dotted `buf.k` read and the static-string-key `buf["k"]` fold. A + // canonical numeric-index string (`buf["2"]`) is still a byte read, + // not a named property (IntegerIndexedExotic `[[Get]]`). + let key_jsval = JSValue::from_bits(index.to_bits()); + if key_jsval.is_string() || key_jsval.is_short_string() { + let key_ptr = js_get_string_pointer_unified(index) as *const crate::StringHeader; + if !key_ptr.is_null() { + if let Some(canon) = unsafe { canonical_buffer_index(key_ptr) } { + let len = unsafe { (*buf).length }; + if canon >= len { + return f64::from_bits(TAG_UNDEFINED); + } + return crate::buffer::js_buffer_get(buf, canon as i32) as f64; + } + return crate::object::js_object_get_field_by_name_f64( + raw_ptr as *const crate::object::ObjectHeader, + key_ptr, + ); + } } - let byte_val = crate::buffer::js_buffer_get(buf, idx_i32); - return byte_val as f64; + return f64::from_bits(TAG_UNDEFINED); } if crate::set::is_registered_set(raw_ptr) || crate::map::is_registered_map(raw_ptr) { let Some(index) = finite_nonnegative_u32_index(index) else { diff --git a/test-files/test_gap_buffer_dyn_key_own_prop_6412.ts b/test-files/test_gap_buffer_dyn_key_own_prop_6412.ts new file mode 100644 index 0000000000..7174b43a15 --- /dev/null +++ b/test-files/test_gap_buffer_dyn_key_own_prop_6412.ts @@ -0,0 +1,39 @@ +// #6412 — a dynamic (any-typed) string key on a Buffer/Uint8Array stores and +// reads an OWN property, not a byte. Node's Buffer is an ordinary Uint8Array, +// so `buf[k]` with a non-numeric `k` is a property, and the numeric fast path +// (loop counters, literal indices) must stay a byte access. +const buf = Buffer.alloc(4); + +// `k` is a string at runtime but `any` statically — the common shape. +const keys: any[] = ["dyn"]; +const k: any = keys[0]; +(buf as any)[k] = "D"; +console.log("dyn-read:", (buf as any)[k]); +console.log("dyn-typeof:", typeof (buf as any)[k]); + +// key out of a call result / Object.keys — also `any`. +function keyName(): any { + return "prop"; +} +(buf as any)[keyName()] = 42; +console.log("call-key-read:", (buf as any)["prop"]); + +// numeric fast path preserved. +(buf as any)[0] = 0x41; +console.log("byte0:", buf[0]); +for (let i = 0; i < 4; i++) { + buf[i] = (i * 3) & 0xff; +} +console.log("loop-bytes:", buf[0], buf[1], buf[2], buf[3]); + +// a method value read through a dynamic key still binds (no own-prop shadow). +console.log("method-typeof:", typeof (buf as any)["readUInt8"]); + +// a dynamic key that IS a canonical index still reads the byte — whether it +// arrives as a number or a canonical numeric-index STRING (`buf["2"]`). +const idx: any = 2; +console.log("dyn-index:", (buf as any)[idx]); +const sidx: any = "2"; +console.log("dyn-str-index:", (buf as any)[sidx]); +const soob: any = "99"; +console.log("dyn-str-oob:", (buf as any)[soob]); diff --git a/test-files/test_gap_buffer_own_prop_shadow_intrinsic_6405.ts b/test-files/test_gap_buffer_own_prop_shadow_intrinsic_6405.ts new file mode 100644 index 0000000000..a0a70aa216 --- /dev/null +++ b/test-files/test_gap_buffer_own_prop_shadow_intrinsic_6405.ts @@ -0,0 +1,29 @@ +// #6405 — an own property SHADOWS the same-named Buffer.prototype method even +// when the call statically folds to the inline byte-read intrinsic. A Buffer is +// an ordinary Uint8Array, so `b.readUInt8 = fn` overrides `readUInt8`. +const b = Buffer.alloc(8); +b[0] = 0xab; +(b as any).readUInt8 = function () { + return "shadowed"; +}; + +// All three call shapes must resolve to the own property. +console.log("dot:", (b as any).readUInt8(0)); +console.log("literal-key:", (b as any)["readUInt8"](0)); +const k = "readUInt8"; +console.log("dyn-key:", (b as any)[k](0)); + +// A method value read (not a call) also returns the override. +console.log("value-typeof:", typeof (b as any).readUInt8); +console.log("value-call:", ((b as any).readUInt8 as () => string)()); + +// An UNSHADOWED buffer still reads the real bytes (via runtime dispatch here, +// since this module shadows a read method somewhere). +const c = Buffer.alloc(4); +c[0] = 0x7f; +c[1] = 0x10; +console.log("unshadowed-u8:", c.readUInt8(0)); +console.log("unshadowed-i16be:", c.readInt16BE(0)); + +// A DIFFERENT read method on the shadowing buffer is not overridden. +console.log("other-method:", (b as any).readInt8(0));