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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ All notable milestones for the nera project are documented in this file.
including a sender paused after its push, and both inbox drain paths. An
empty-inbox control retains the initial process's wait and links.

## Deep term operations use heap-allocated work stacks

- `<` and `>` comparisons, `term_to_binary/1`, `iolist_to_binary/1` and
`erlang:phash2/1` handle iolists nested 200,000 levels deep in head
position without overflowing the native stack. A shared native/BEAM
regression checks ordering, complete encoded bytes and flattened output.
- `erlang:phash2/1,2` now use OTP's portable data-term hashing instead of
Nera's FNV-style hash, including byte-list packing, binary blocks and
order-independent map pairs. Previously stored Nera hash values may change;
fun hashes still lack BEAM's function metadata.

## Oversized binary copies raise Erlang errors

- `binary:copy/2` checks result-size multiplication and the binary header's
Expand Down
173 changes: 20 additions & 153 deletions crates/nera-runtime/src/bif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,146 +281,6 @@ pub unsafe fn bif_byte_size(_heap: &mut ProcessHeap, args: &[ErlTerm]) -> Result
// Hashing
// ---------------------------------------------------------------------------

/// FNV-1a-style hash mixing.
#[inline]
fn hash_mix(hash: u64, value: u64) -> u64 {
(hash ^ value).wrapping_mul(16777619)
}

/// Compute a deterministic hash of an Erlang term.
///
/// Integers equal to floats must produce the same hash (e.g. 1 and 1.0).
/// # Safety
/// Source graphs obey the enclosing BIF live, immutable, unmoved input contract.
unsafe fn hash_erlang_term(term: ErlTerm) -> u64 {
// Small integer
if let Some(n) = term.as_small_int() {
return hash_mix(0xcbf29ce484222325, n as u64);
}

// Atom
if let Some(id) = term.as_atom_id() {
return hash_mix(0xcbf29ce484222325 ^ 0xa1, id as u64);
}

// Pid
if term.is_pid() {
return hash_mix(0xcbf29ce484222325 ^ 0xb1, term.0);
}

// Nil
if term.is_nil() {
return 0xcbf29ce484222325 ^ 0xc1;
}

// Cons cell — iterate to avoid stack overflow
if term.is_cons() {
let mut hash = 0xcbf29ce484222325_u64 ^ 0xd1;
let mut current = term;
while current.is_cons() {
if let Some(ptr) = current.as_cons_ptr() {
unsafe {
hash = hash_mix(hash, hash_erlang_term((*ptr).head));
current = (*ptr).tail;
}
} else {
break;
}
}
// Hash the tail (nil or improper)
if !current.is_nil() {
hash = hash_mix(hash, hash_erlang_term(current));
}
return hash;
}

// Boxed types
if term.is_boxed() {
if let Some(ptr) = term.as_boxed_ptr() {
unsafe {
let header = BoxedHeader(*ptr);
match header.subtype() {
BOXED_FLOAT => {
let bits = *ptr.add(1);
let val = f64::from_bits(bits);
// If float is a whole number, hash it as integer for consistency
if val.fract() == 0.0 && val.is_finite() {
return hash_mix(0xcbf29ce484222325, val as i64 as u64);
}
return hash_mix(0xcbf29ce484222325 ^ 0xe1, bits);
}
BOXED_TUPLE => {
let arity = header.arity() as usize;
let mut hash = hash_mix(0xcbf29ce484222325 ^ 0xf1, arity as u64);
for i in 1..=arity {
hash = hash_mix(hash, hash_erlang_term(ErlTerm(*ptr.add(i))));
}
return hash;
}
BOXED_BINARY | BOXED_REFC_BINARY => {
let size = header.arity() as usize;
let byte_len = size.div_ceil(8);
let data_ptr = if header.subtype() == BOXED_BINARY {
(ptr.add(1)) as *const u8
} else {
// Read the stable backing object to get the data pointer.
// The raw word is a *const BinaryData, not a direct data pointer.
let raw_arc_ptr = *ptr.add(REFC_BINARY_PTR_WORD_INDEX) as *const BinaryData;
let offset = *ptr.add(REFC_BINARY_OFFSET_WORD_INDEX) as usize;
let vec_ref = &*raw_arc_ptr;
vec_ref.as_ptr().add(offset)
};
let mut hash = 0xcbf29ce484222325_u64 ^ 0xa2;
for i in 0..byte_len {
hash = hash_mix(hash, *data_ptr.add(i) as u64);
}
return hash;
}
BOXED_MAP => {
let num_pairs = header.arity() as usize;
// Order-independent: XOR individual pair hashes
let mut hash = 0_u64;
for i in 0..num_pairs {
let key = ErlTerm(*ptr.add(1 + i * 2));
let val = ErlTerm(*ptr.add(2 + i * 2));
let pair_hash = hash_mix(
hash_erlang_term(key),
hash_erlang_term(val),
);
hash ^= pair_hash;
}
return hash_mix(0xcbf29ce484222325 ^ 0xb2, hash);
}
BOXED_BIGINT => {
let limb_count = header.arity() as usize;
let mut hash = 0xcbf29ce484222325_u64 ^ 0xc2;
// Hash sign word
hash = hash_mix(hash, *ptr.add(1));
// Hash limbs
for i in 0..limb_count {
hash = hash_mix(hash, *ptr.add(2 + i));
}
return hash;
}
BOXED_REFERENCE => {
return hash_mix(0xcbf29ce484222325 ^ 0xd2, *ptr.add(1));
}
BOXED_CLOSURE => {
return hash_mix(0xcbf29ce484222325 ^ 0xe2, *ptr.add(1));
}
_ => {
// Unknown boxed type — hash raw bits
return hash_mix(0xcbf29ce484222325 ^ 0xff, term.0);
}
}
}
}
}

// Fallback
hash_mix(0xcbf29ce484222325, term.0)
}

/// `erlang:phash2/1` -- portable hash of an Erlang term.
///
/// Returns an integer in the range 0..(2^27 - 1).
Expand All @@ -432,7 +292,7 @@ pub unsafe fn bif_phash2(_heap: &mut ProcessHeap, args: &[ErlTerm]) -> Result<Er
if args.len() != 1 {
return Err(ErlTerm::atom(ATOM_BADARG));
}
let hash = hash_erlang_term(args[0]);
let hash = crate::phash2::hash(args[0]);
let result = (hash % (1 << 27)) as i64;
Ok(ErlTerm::small_int(result))
}
Expand All @@ -452,7 +312,7 @@ pub unsafe fn bif_phash2_range(_heap: &mut ProcessHeap, args: &[ErlTerm]) -> Res
Some(r) if r > 0 => r as u64,
_ => return Err(ErlTerm::atom(ATOM_BADARG)),
};
let hash = hash_erlang_term(args[0]);
let hash = u64::from(crate::phash2::hash(args[0]));
let result = (hash % range) as i64;
Ok(ErlTerm::small_int(result))
}
Expand Down Expand Up @@ -1647,6 +1507,15 @@ const NEW_PID_EXT: u8 = 88;
/// # Safety
/// Source graphs obey the enclosing BIF live, immutable, unmoved input contract.
unsafe fn etf_encode(term: ErlTerm, buf: &mut Vec<u8>) {
let mut pending = vec![term];
while let Some(term) = pending.pop() {
etf_encode_next(term, buf, &mut pending);
}
}

// Schedule children in reverse wire order without borrowing the process heap
// mutably: these raw terms cannot move while the work stack is being drained.
unsafe fn etf_encode_next(term: ErlTerm, buf: &mut Vec<u8>, pending: &mut Vec<ErlTerm>) {
// Small integer
if let Some(val) = term.as_small_int() {
if (0..=255).contains(&val) {
Expand Down Expand Up @@ -1739,11 +1608,9 @@ unsafe fn etf_encode(term: ErlTerm, buf: &mut Vec<u8>) {

buf.push(LIST_EXT);
buf.extend_from_slice(&(elements.len() as u32).to_be_bytes());
for e in &elements {
etf_encode(*e, buf);
}
// Tail (nil for proper list, or the improper tail)
etf_encode(cursor, buf);
// Tail follows every element, including for improper lists.
pending.push(cursor);
pending.extend(elements.into_iter().rev());
return;
}

Expand All @@ -1766,8 +1633,8 @@ unsafe fn etf_encode(term: ErlTerm, buf: &mut Vec<u8>) {
buf.push(LARGE_TUPLE_EXT);
buf.extend_from_slice(&(arity as u32).to_be_bytes());
}
for i in 1..=arity {
etf_encode(ErlTerm(*ptr.add(i)), buf);
for i in (1..=arity).rev() {
pending.push(ErlTerm(*ptr.add(i)));
}
}
BOXED_BINARY | BOXED_REFC_BINARY => {
Expand All @@ -1781,11 +1648,11 @@ unsafe fn etf_encode(term: ErlTerm, buf: &mut Vec<u8>) {
let npairs = header.arity() as usize;
buf.push(MAP_EXT);
buf.extend_from_slice(&(npairs as u32).to_be_bytes());
for i in crate::ffi::compare::map_iteration_order(ptr, npairs) {
for i in crate::ffi::compare::map_iteration_order(ptr, npairs).into_iter().rev() {
let key = ErlTerm(*ptr.add(1 + i * 2));
let val = ErlTerm(*ptr.add(2 + i * 2));
etf_encode(key, buf);
etf_encode(val, buf);
pending.push(val);
pending.push(key);
}
}
BOXED_BIGINT => {
Expand Down Expand Up @@ -1814,7 +1681,7 @@ unsafe fn etf_encode(term: ErlTerm, buf: &mut Vec<u8>) {
buf.extend_from_slice(b"ref");
// id as integer
let id = *ptr.add(1);
etf_encode(ErlTerm::small_int(id as i64), buf);
pending.push(ErlTerm::small_int(id as i64));
}
_ => {
// Unsupported boxed type — encode as atom 'undefined'
Expand Down
82 changes: 68 additions & 14 deletions crates/nera-runtime/src/ffi/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,65 @@ pub(crate) unsafe fn map_iteration_order(ptr: *const u64, npairs: usize) -> Vec<
order
}

enum CompareWork {
Terms(ErlTerm, ErlTerm, bool),
TupleFields { a: ErlTerm, b: ErlTerm, next: usize, exact: bool },
MapFields(Box<MapComparison>),
}

struct MapComparison {
a: Vec<(ErlTerm, ErlTerm)>,
b: Vec<(ErlTerm, ErlTerm)>,
next: usize,
exact: bool,
}

/// Map keys require exact ordering throughout their graph, including map values
/// nested inside a key. Ordinary map values retain the caller's comparison mode.
/// The term lifetime contract is the same as compare_terms's.
unsafe fn compare_terms_mode(a: ErlTerm, b: ErlTerm, exact: bool) -> i64 {
let mut pending = Vec::new();
let mut current = CompareWork::Terms(a, b, exact);
loop {
let (a, b, exact) = match current {
CompareWork::Terms(a, b, exact) => (a, b, exact),
CompareWork::TupleFields { a, b, next, exact } => {
let pa = a.as_boxed_ptr().unwrap();
let pb = b.as_boxed_ptr().unwrap();
if next < BoxedHeader(*pa).arity() as usize {
pending.push(CompareWork::TupleFields { a, b, next: next + 1, exact });
}
(ErlTerm(*pa.add(next)), ErlTerm(*pb.add(next)), exact)
}
CompareWork::MapFields(mut fields) => {
let n = fields.a.len();
let index = fields.next % n;
let keys = fields.next < n;
let (a, b, exact) = if keys {
(fields.a[index].0, fields.b[index].0, true)
} else {
(fields.a[index].1, fields.b[index].1, fields.exact)
};
fields.next += 1;
if fields.next < 2 * n {
pending.push(CompareWork::MapFields(fields));
}
(a, b, exact)
}
};
let result = compare_terms_step(a, b, exact, &mut pending);
if result != 0 { return result; }
let Some(next) = pending.pop() else { return 0; };
current = next;
}
}

// Equal structural prefixes save one indexed continuation per container,
// not every remaining field. Wide tuples still stop at the first mismatch.
unsafe fn compare_terms_step(
a: ErlTerm, b: ErlTerm, exact: bool,
pending: &mut Vec<CompareWork>,
) -> i64 {
use crate::term::*;

// Fast path: identical raw values
Expand Down Expand Up @@ -210,12 +265,12 @@ unsafe fn compare_terms_mode(a: ErlTerm, b: ErlTerm, exact: bool) -> i64 {
if b_num { return 1; } // non-number > number

// Same-type structural comparison
// Cons cells: compare heads, then tails recursively
// Cons cells: compare heads before tails
if let (Some(pa), Some(pb)) = (a.as_cons_ptr(), b.as_cons_ptr()) {
unsafe {
let r = compare_terms_mode((*pa).head, (*pb).head, exact);
if r != 0 { return r; }
return compare_terms_mode((*pa).tail, (*pb).tail, exact);
pending.push(CompareWork::Terms((*pa).tail, (*pb).tail, exact));
pending.push(CompareWork::Terms((*pa).head, (*pb).head, exact));
return 0;
}
}

Expand All @@ -229,9 +284,11 @@ unsafe fn compare_terms_mode(a: ErlTerm, b: ErlTerm, exact: bool) -> i64 {
let aa = ha.arity();
let ab = hb.arity();
if aa != ab { return if aa < ab { -1 } else { 1 }; }
for i in 1..=aa as usize {
let r = compare_terms_mode(ErlTerm(*pa.add(i)), ErlTerm(*pb.add(i)), exact);
if r != 0 { return r; }
if aa > 0 {
if aa > 1 {
pending.push(CompareWork::TupleFields { a, b, next: 2, exact });
}
pending.push(CompareWork::Terms(ErlTerm(*pa.add(1)), ErlTerm(*pb.add(1)), exact));
}
return 0;
}
Expand Down Expand Up @@ -263,13 +320,10 @@ unsafe fn compare_terms_mode(a: ErlTerm, b: ErlTerm, exact: bool) -> i64 {
r if r > 0 => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
});
for i in 0..na {
let r = compare_terms_mode(ka[i].0, kb[i].0, true);
if r != 0 { return r; }
}
for i in 0..na {
let r = compare_terms_mode(ka[i].1, kb[i].1, exact);
if r != 0 { return r; }
if na > 0 {
pending.push(CompareWork::MapFields(Box::new(MapComparison {
a: ka, b: kb, next: 0, exact,
})));
}
return 0;
}
Expand Down
Loading
Loading