Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions changelog.d/6896-sqlite-node26-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**SQLite:** Complete Node.js 26 compatibility for database configuration, parameter binding, serialization, limits, aggregates, sessions, and statement iteration.
18 changes: 18 additions & 0 deletions crates/perry-codegen/src/lower_call/native_table/databases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,24 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[
args: &[NA_F64, NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "sqlite",
has_receiver: true,
method: "serialize",
class_filter: None,
runtime: "js_node_sqlite_database_sync_serialize",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "sqlite",
has_receiver: true,
method: "deserialize",
class_filter: None,
runtime: "js_node_sqlite_database_sync_deserialize",
args: &[NA_F64],
ret: NR_VOID,
},
NativeModSig {
module: "sqlite",
has_receiver: true,
Expand Down
23 changes: 14 additions & 9 deletions crates/perry-codegen/src/stmt/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5079,6 +5079,7 @@ fn lower_for_after_init_with_i32_bound(
if let Some(cond_expr) = condition {
let cv = lower_expr(ctx, cond_expr)?;
let i1 = lower_truthy(ctx, &cv, cond_expr);
emit_gc_loop_safepoint(ctx, &[], &[cond_expr]);
ctx.block().cond_br(&i1, &body_label, &exit_label);
} else {
ctx.block().br(&body_label);
Expand All @@ -5091,6 +5092,7 @@ fn lower_for_after_init_with_i32_bound(
if let Some(cond_expr) = condition {
let cv = lower_expr(ctx, cond_expr)?;
let i1 = lower_truthy(ctx, &cv, cond_expr);
emit_gc_loop_safepoint(ctx, &[], &[cond_expr]);
ctx.block().cond_br(&i1, &body_label, &exit_label);
} else {
// `for (;;)` — unconditional jump into the body. May be an
Expand Down Expand Up @@ -5140,15 +5142,15 @@ fn lower_for_after_init_with_i32_bound(
ctx.block().asm_sideeffect_barrier();
}
if !ctx.block().is_terminated() {
let controls: Vec<&perry_hir::Expr> = condition.into_iter().chain(update).collect();
emit_gc_loop_safepoint(ctx, body, &controls);
emit_gc_loop_safepoint(ctx, body, &[]);
ctx.block().br(&update_label);
}

// Update block.
ctx.current_block = update_idx;
if let Some(update_expr) = update {
let _ = lower_expr(ctx, update_expr)?;
emit_gc_loop_safepoint(ctx, &[], &[update_expr]);
}
// #6072: a loop-private i32 counter is invisible to the `Update` lowering
// (it is not in `ctx.i32_counter_slots`), so advance it here. The classifier
Expand Down Expand Up @@ -5238,11 +5240,12 @@ fn moving_safepoint_polls_enabled() -> bool {
})
}

/// Emit a `js_gc_loop_safepoint()` poll at a loop back-edge. Call this AFTER
/// `clear_loop_body_shadow_slots` and only where the block is not terminated:
/// at that point the loop-body expression has completed, so every live heap
/// value is a named local on the shadow stack (no unspilled register temps) —
/// a precise-root safepoint where a deferred copying minor can MOVE survivors.
/// Emit a `js_gc_loop_safepoint()` after an allocating loop segment has
/// completed and only where the block is not terminated. Body calls must run
/// after `clear_loop_body_shadow_slots`; control calls run after their result
/// has been reduced or discarded. At either point every live heap value is a
/// named local on the shadow stack (no unspilled register temps) — a precise
/// root safepoint where a deferred copying minor can MOVE survivors.
///
/// COVERAGE (Phase 2, follow-up): currently wired into the generic `while`,
/// `do..while`, and `for` back-edges. The specialized/versioned `for`-loop
Expand Down Expand Up @@ -7009,6 +7012,7 @@ pub(crate) fn lower_while(
ctx.current_block = cond_idx;
let cv = lower_expr(ctx, condition)?;
let i1 = lower_truthy(ctx, &cv, condition);
emit_gc_loop_safepoint(ctx, &[], &[condition]);
ctx.block().cond_br(&i1, &body_label, &exit_label);

// For while-loops, continue jumps back to the cond block.
Expand Down Expand Up @@ -7046,7 +7050,7 @@ pub(crate) fn lower_while(
ctx.block().asm_sideeffect_barrier();
}
if !ctx.block().is_terminated() {
emit_gc_loop_safepoint(ctx, body, &[condition]);
emit_gc_loop_safepoint(ctx, body, &[]);
ctx.block().br(&cond_label);
}
ctx.active_region_id = previous_region_id;
Expand Down Expand Up @@ -7104,13 +7108,14 @@ pub(crate) fn lower_do_while(
ctx.block().asm_sideeffect_barrier();
}
if !ctx.block().is_terminated() {
emit_gc_loop_safepoint(ctx, body, &[condition]);
emit_gc_loop_safepoint(ctx, body, &[]);
ctx.block().br(&cond_label);
}

ctx.current_block = cond_idx;
let cv = lower_expr(ctx, condition)?;
let i1 = lower_truthy(ctx, &cv, condition);
emit_gc_loop_safepoint(ctx, &[], &[condition]);
ctx.block().cond_br(&i1, &body_label, &exit_label);
ctx.active_region_id = previous_region_id;

Expand Down
78 changes: 69 additions & 9 deletions crates/perry-runtime/src/array/iter_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,33 @@ pub fn array_values_iter(arr_f64: f64) -> f64 {
/// Values iterator whose done-result carries `value: null` and whose
/// `return()` terminates it — the `node:sqlite` `iterate()` protocol
/// (#6561). See [`KIND_VALUES_NULL_DONE`].
pub fn array_values_iter_null_done(arr_f64: f64) -> f64 {
pub fn array_values_iter_null_done(
arr_f64: f64,
iteration_epoch: &std::sync::atomic::AtomicU64,
epoch: u64,
) -> f64 {
let arr_ptr = unbox_array_ptr(arr_f64);
if arr_ptr.is_null() {
return f64::from_bits(TAG_UNDEFINED);
}
unsafe { alloc_iterator(arr_ptr, KIND_VALUES_NULL_DONE) }
unsafe {
let obj = js_object_alloc(ARRAY_ITERATOR_CLASS_ID, 5);
js_object_set_field(
obj,
0,
JSValue::from_bits(js_nanbox_pointer(arr_ptr as i64).to_bits()),
);
js_object_set_field(obj, 1, JSValue::number(0.0));
js_object_set_field(obj, 2, JSValue::number(KIND_VALUES_NULL_DONE as f64));
js_object_set_field(
obj,
3,
JSValue::pointer(iteration_epoch as *const _ as *const u8),
);
js_object_set_field(obj, 4, JSValue::number(epoch as f64));
crate::object::attach_iterator_prototype(obj, ARRAY_ITERATOR_CLASS_ID);
js_nanbox_pointer(obj as i64)
}
}

/// `arr.keys()` iterator — yields each index `0..length`.
Expand Down Expand Up @@ -517,6 +538,19 @@ unsafe fn make_iter_result(value: JSValue, done: bool) -> f64 {
js_nanbox_pointer(obj as i64)
}

unsafe fn make_sqlite_iter_result(value: JSValue, done: bool) -> f64 {
let obj = js_object_alloc(0, 2);
let done_key = crate::string::js_string_from_bytes(b"done".as_ptr(), 4);
let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5);
let keys = crate::array::js_array_alloc(2);
crate::array::js_array_push(keys, JSValue::string_ptr(done_key));
crate::array::js_array_push(keys, JSValue::string_ptr(value_key));
crate::object::js_object_set_keys(obj, keys);
js_object_set_field(obj, 0, JSValue::bool(done));
js_object_set_field(obj, 1, value);
js_nanbox_pointer(obj as i64)
}

unsafe fn make_pair_array(idx: u32, value: f64) -> f64 {
let pair = crate::array::js_array_alloc(2);
(*pair).length = 2;
Expand Down Expand Up @@ -547,15 +581,29 @@ pub unsafe fn dispatch_array_iterator_method(
};
match method_name {
"next" => {
if kind == KIND_VALUES_NULL_DONE {
let epoch_ptr =
js_nanbox_get_pointer(f64::from_bits(js_object_get_field(iter_obj, 3).bits()))
as *const std::sync::atomic::AtomicU64;
let expected = f64::from_bits(js_object_get_field(iter_obj, 4).bits()) as u64;
if epoch_ptr.is_null()
|| (*epoch_ptr).load(std::sync::atomic::Ordering::Relaxed) != expected
{
crate::fs::validate::throw_error_with_code(
"Statement iterator has been invalidated",
"ERR_INVALID_STATE",
);
}
}
// Field 0: backing array pointer (NaN-boxed).
let backing_field = js_object_get_field(iter_obj, 0);
let backing_f64 = f64::from_bits(backing_field.bits());
// Once the iterator is exhausted the backing array is cleared to
// `undefined` (spec: `[[IteratedArrayLike]]` set to undefined), so a
// later `.next()` stays done even if the array grew after exhaustion
// (test262 Array/prototype/{values,keys,entries}/iteration-mutable:
// pushing AFTER the iterator reported done must not resurface).
// Array iterators clear their backing array at exhaustion. SQLite's
// statement iterator restarts a completed execution on the next call.
if JSValue::from_bits(backing_f64.to_bits()).is_undefined() {
if kind == KIND_VALUES_NULL_DONE {
return make_sqlite_iter_result(done_value(), true);
}
return make_iter_result(done_value(), true);
}
let arr_ptr = js_nanbox_get_pointer(backing_f64) as *const ArrayHeader;
Expand All @@ -570,6 +618,10 @@ pub unsafe fn dispatch_array_iterator_method(
};

if idx >= len {
if kind == KIND_VALUES_NULL_DONE {
js_object_set_field(iter_obj, 1, JSValue::number(0.0));
return make_sqlite_iter_result(done_value(), true);
}
js_object_set_field(iter_obj, 0, JSValue::undefined());
return make_iter_result(done_value(), true);
}
Expand All @@ -593,7 +645,11 @@ pub unsafe fn dispatch_array_iterator_method(
}
_ => JSValue::undefined(),
};
make_iter_result(value, false)
if kind == KIND_VALUES_NULL_DONE {
make_sqlite_iter_result(value, false)
} else {
make_iter_result(value, false)
}
}
// Iterators are themselves iterable — `[Symbol.iterator]()` on one
// returns the same iterator (matches Node, and lets `js_get_iterator`
Expand All @@ -609,7 +665,11 @@ pub unsafe fn dispatch_array_iterator_method(
if kind == KIND_VALUES_NULL_DONE {
js_object_set_field(iter_obj, 0, JSValue::undefined());
}
make_iter_result(done_value(), true)
if kind == KIND_VALUES_NULL_DONE {
make_sqlite_iter_result(done_value(), true)
} else {
make_iter_result(done_value(), true)
}
}
_ => f64::from_bits(TAG_UNDEFINED),
}
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-stdlib/src/common/dispatch/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ pub unsafe extern "C" fn js_handle_property_set_dispatch(

#[no_mangle]
pub unsafe extern "C" fn js_handle_own_property_names_dispatch(handle: i64) -> f64 {
#[cfg(feature = "database-sqlite")]
if let Some(names) = crate::sqlite::dispatch_node_sqlite_own_property_names(handle) {
return names;
}
if crate::string_decoder::is_string_decoder_handle(handle) {
return crate::string_decoder::string_decoder_own_property_names(handle);
}
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-stdlib/src/common/dispatch/method_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ pub unsafe extern "C" fn js_handle_method_dispatch(
| "close"
| "exec"
| "prepare"
| "serialize"
| "deserialize"
// `function`/`aggregate`/`enableDefensive`/`setAuthorizer` were
// missing from this gate (#6561): an any-typed
// `db.function(...)` / `db.aggregate(...)` fell through the
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-stdlib/src/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use crate::common::{for_each_handle_mut_of, Handle};
use rusqlite::Connection;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Mutex, Once, OnceLock};

mod backup;
Expand Down Expand Up @@ -142,6 +142,7 @@ pub struct NodeSqliteStmtHandle {
pub db_handle: Handle,
pub sql: String,
pub finalized: AtomicBool,
pub iteration_epoch: AtomicU64,
pub read_bigints: AtomicBool,
pub return_arrays: AtomicBool,
pub allow_bare_named_parameters: AtomicBool,
Expand Down
Loading
Loading