fix(hir+codegen): chained Buffer.concat(x).slice(...) → real Buffer.slice (#1177) - #1180
Merged
Merged
Conversation
…ffer.slice `Buffer.concat(chunks).slice(0, 8)` written inline (no intermediate `const`) returned a "string" of length 8 whose bytes were all empty/undefined — the user-visible bug from the #1132 verification detour. Storing the concat result in a local first made `.slice` work correctly, which is why the bug went unnoticed (every example in real code happened to use the local-bound form, until someone wrote the natural one-liner). ## Root cause The buffer-method dispatch block at `crates/perry-hir/src/lower/expr_call/mod.rs:3809` is guarded by `if let ast::Expr::Ident(arr_ident) = member.obj.as_ref()` — it only fires when the `.slice` receiver is a bare identifier. For a chained `Buffer.concat(chunks).slice(0,8)` the receiver is itself a `CallExpr`, so the buffer-aware path is skipped. The `.slice` arm at `:5396` then folded receivers like `ArrayMap`, `ArraySort`, `Array(_)`, etc. to `Expr::ArraySlice`, but `BufferConcat` / `BufferFrom` / `BufferSlice` weren't in the list. Generic Call fallthrough routed `.slice` through `js_native_call_method` which picked String.slice semantics on the NaN-boxed Buffer pointer — producing `typeof === "string"`, `isBuffer === false`, `length=8`, all bytes empty. ## Fix Two small pieces: 1. **HIR fold** (`expr_call/mod.rs:5396`): before the existing ArraySlice fold, recognize `BufferConcat` / `BufferFrom` / `BufferSlice` receivers and emit `Expr::BufferSlice { buffer, start, end }`. Must come BEFORE the Array shapes so a Buffer receiver isn't misrouted to `ArraySlice` (which would call `js_array_slice` on a Buffer pointer). 2. **Codegen** (`expr/mod.rs`): `Expr::BufferSlice` had a HIR variant declared and walked but no codegen lowering. Added an arm that lowers start/end to i32 (default `0` and `i32::MAX` for the "no end" case — `js_buffer_slice` clamps end to length) and calls `js_buffer_slice` directly, NaN-boxing the resulting fresh `BufferHeader` with POINTER_TAG. `js_buffer_slice` already `ptr::copy_nonoverlapping`'s bytes into a newly allocated Buffer registered in BUFFER_REGISTRY, so the result has its own backing storage independent of the parent's lifetime. ## Verified - Issue-body repro `Array.from(Buffer.concat(chunks).slice(0, 8)).join(",")` → `137,80,78,71,13,10,26,10` ✓ (was `, , , , , , , `) - `typeof Buffer.concat(c).slice(0,8) === "object"` ✓ (was `"string"`) - `Buffer.isBuffer(Buffer.concat(c).slice(0,8)) === true` ✓ (was `false`) - Chain of slices `Buffer.concat(c).slice(0,8).slice(2,4)` → `78,71` ✓ - `Buffer.from([1,2,3,4,5]).slice(1,4)` → `2,3,4` ✓ - Default-end `Buffer.concat(c).slice(2)` → `78,71,13,10,26,10` ✓ - Pre-existing stored-then-slice form `const b = Buffer.concat(c); b.slice(0,8)` still works (regression check) ✓ - Existing #1132 verification repro (`http.createServer` → `http.get` → `chunks` → `slice`) unchanged - `cargo test --release -p perry-hir -p perry-codegen` clean
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Fixes #1177.
Buffer.concat(chunks).slice(0, 8)written inline (no intermediateconst) returned a "string" of length 8 whose bytes were all empty/undefined. Storing the concat result in a local first made.slicework — which is why the bug went unnoticed: every example in real code happened to use the local-bound form, until someone wrote the natural one-liner during the #1132 verification detour.Root cause
The buffer-method dispatch block at
crates/perry-hir/src/lower/expr_call/mod.rs:3809is guarded byif let ast::Expr::Ident(arr_ident) = member.obj.as_ref()— it only fires when the.slicereceiver is a bare identifier. For chainedBuffer.concat(chunks).slice(0,8)the receiver is itself aCallExpr, so the buffer-aware path is skipped.The
.slicearm at:5396folded receivers likeArrayMap,ArraySort,Array(_), etc. toExpr::ArraySlice, butBufferConcat/BufferFrom/BufferSliceweren't in the list. Generic Call fallthrough routed.slicethroughjs_native_call_methodwhich picked String.slice semantics on the NaN-boxed Buffer pointer — producingtypeof === "string",isBuffer === false,length=8, all bytes empty.Fix
expr_call/mod.rs:5396): before the existing ArraySlice fold, recognizeBufferConcat/BufferFrom/BufferSlicereceivers and emitExpr::BufferSlice { buffer, start, end }. Must come before the Array shapes so a Buffer receiver isn't misrouted toArraySlice(which would calljs_array_sliceon a Buffer pointer).expr/mod.rs):Expr::BufferSlicehad a HIR variant declared and walked but no codegen lowering. Added an arm that lowers start/end to i32 (default0andi32::MAXfor the "no end" case —js_buffer_sliceclamps end to length) and callsjs_buffer_slicedirectly.js_buffer_slicealreadyptr::copy_nonoverlapping's bytes into a newly allocated Buffer registered in BUFFER_REGISTRY, so the result has its own backing storage independent of the parent's lifetime.Test plan
Array.from(Buffer.concat(chunks).slice(0, 8)).join(",")→137,80,78,71,13,10,26,10(was, , , , , , ,)typeof Buffer.concat(c).slice(0,8) === "object"(was"string")Buffer.isBuffer(Buffer.concat(c).slice(0,8)) === true(wasfalse)Buffer.concat(c).slice(0,8).slice(2,4)→78,71Buffer.from([1,2,3,4,5]).slice(1,4)→2,3,4Buffer.concat(c).slice(2)→78,71,13,10,26,10const b = Buffer.concat(c); b.slice(0,8)still works (regression check)http.createServer→http.get→chunks→slice) unchangedcargo test --release -p perry-hir -p perry-codegenclean