Skip to main content

dazzle_core/scheme/
primitives.rs

1//! Arena-based primitive implementations (Phase 1 prototype)
2//!
3//! This module contains arena-based implementations of the hottest primitives
4//! to benchmark the performance gain from eliminating Gc<T> overhead.
5//!
6//! Hot primitives (from DocBook profiling):
7//! - equal?: 4,496 calls
8//! - car: 4,221 calls
9//! - null?: 3,968 calls
10//! - cdr: 3,505 calls
11//! - cons: ~3,000 calls
12
13use crate::scheme::arena::{Arena, ValueId, ValueData, NIL_ID};
14use std::rc::Rc;
15
16pub type ArenaResult = Result<ValueId, String>;
17
18/// (car pair) → value
19///
20/// Arena version: Zero overhead - just copy u32
21#[inline]
22pub fn car(arena: &Arena, args: &[ValueId]) -> ArenaResult {
23    if args.len() != 1 {
24        return Err("car requires exactly 1 argument".to_string());
25    }
26
27    match arena.get(args[0]) {
28        ValueData::Pair { car, .. } => Ok(*car),
29        _ => Err(format!("car: not a pair")),
30    }
31}
32
33/// (cdr pair) → value
34///
35/// Arena version: Zero overhead - just copy u32
36#[inline]
37pub fn cdr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
38    if args.len() != 1 {
39        return Err("cdr requires exactly 1 argument".to_string());
40    }
41
42    match arena.get(args[0]) {
43        ValueData::Pair { cdr, .. } => Ok(*cdr),
44        _ => Err(format!("cdr: not a pair")),
45    }
46}
47
48/// (cons obj1 obj2) → pair
49///
50/// Arena version: Direct arena allocation
51#[inline]
52pub fn cons(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
53    if args.len() != 2 {
54        return Err("cons requires exactly 2 arguments".to_string());
55    }
56
57    Ok(arena.cons(args[0], args[1]))
58}
59
60/// (null? obj) → boolean
61///
62/// Arena version: Simple equality check
63#[inline]
64pub fn null(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
65    if args.len() != 1 {
66        return Err("null? requires exactly 1 argument".to_string());
67    }
68
69    Ok(if args[0] == NIL_ID {
70        crate::scheme::arena::TRUE_ID
71    } else {
72        crate::scheme::arena::FALSE_ID
73    })
74}
75
76/// (equal? obj1 obj2) → boolean
77///
78/// Arena version: Recursive structural equality
79pub fn equal(arena: &Arena, args: &[ValueId]) -> ArenaResult {
80    if args.len() != 2 {
81        return Err("equal? requires exactly 2 arguments".to_string());
82    }
83
84    Ok(if equal_impl(arena, args[0], args[1]) {
85        crate::scheme::arena::TRUE_ID
86    } else {
87        crate::scheme::arena::FALSE_ID
88    })
89}
90
91/// Implementation of structural equality
92fn equal_impl(arena: &Arena, a: ValueId, b: ValueId) -> bool {
93    // Fast path: same ID
94    if a == b {
95        return true;
96    }
97
98    match (arena.get(a), arena.get(b)) {
99        // Structural equality for pairs
100        (ValueData::Pair { car: car1, cdr: cdr1, .. },
101         ValueData::Pair { car: car2, cdr: cdr2, .. }) => {
102            equal_impl(arena, *car1, *car2) && equal_impl(arena, *cdr1, *cdr2)
103        }
104
105        // Structural equality for vectors
106        (ValueData::Vector(v1), ValueData::Vector(v2)) => {
107            v1.len() == v2.len() &&
108            v1.iter().zip(v2.iter()).all(|(a, b)| equal_impl(arena, *a, *b))
109        }
110
111        // Value equality for primitives
112        (ValueData::Integer(n1), ValueData::Integer(n2)) => n1 == n2,
113        (ValueData::Real(f1), ValueData::Real(f2)) => f1 == f2,
114        (ValueData::Bool(b1), ValueData::Bool(b2)) => b1 == b2,
115        (ValueData::Char(c1), ValueData::Char(c2)) => c1 == c2,
116        (ValueData::String(s1), ValueData::String(s2)) => s1 == s2,
117        (ValueData::Symbol(s1), ValueData::Symbol(s2)) => s1 == s2,
118
119        _ => false,
120    }
121}
122
123// =============================================================================
124// Batch 1: List Operations (Phase 3)
125// =============================================================================
126
127/// (cadr list) → value
128/// Equivalent to (car (cdr list))
129#[inline]
130pub fn cadr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
131    if args.len() != 1 {
132        return Err("cadr requires exactly 1 argument".to_string());
133    }
134    let cdr_result = cdr(arena, args)?;
135    car(arena, &[cdr_result])
136}
137
138/// (caddr list) → value
139/// Equivalent to (car (cdr (cdr list)))
140#[inline]
141pub fn caddr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
142    if args.len() != 1 {
143        return Err("caddr requires exactly 1 argument".to_string());
144    }
145    let cdr1 = cdr(arena, args)?;
146    let cdr2 = cdr(arena, &[cdr1])?;
147    car(arena, &[cdr2])
148}
149
150/// (cadddr list) → value
151/// Equivalent to (car (cdr (cdr (cdr list))))
152#[inline]
153pub fn cadddr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
154    if args.len() != 1 {
155        return Err("cadddr requires exactly 1 argument".to_string());
156    }
157    let cdr1 = cdr(arena, args)?;
158    let cdr2 = cdr(arena, &[cdr1])?;
159    let cdr3 = cdr(arena, &[cdr2])?;
160    car(arena, &[cdr3])
161}
162
163/// (list obj ...) → list
164/// Create a list from arguments
165pub fn list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
166    let mut result = NIL_ID;
167    for &arg in args.iter().rev() {
168        result = arena.cons(arg, result);
169    }
170    Ok(result)
171}
172
173/// (length list) → integer
174/// Return the length of a list
175pub fn length(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
176    if args.len() != 1 {
177        return Err("length requires exactly 1 argument".to_string());
178    }
179
180    let mut len = 0i64;
181    let mut current = args[0];
182
183    loop {
184        if current == NIL_ID {
185            return Ok(arena.int(len));
186        }
187
188        match arena.get(current) {
189            ValueData::Pair { cdr, .. } => {
190                len += 1;
191                current = *cdr;
192            }
193            _ => return Err("length: not a proper list".to_string()),
194        }
195    }
196}
197
198/// (reverse list) → list
199/// Reverse a list
200pub fn reverse(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
201    if args.len() != 1 {
202        return Err("reverse requires exactly 1 argument".to_string());
203    }
204
205    let mut result = NIL_ID;
206    let mut current = args[0];
207
208    loop {
209        if current == NIL_ID {
210            return Ok(result);
211        }
212
213        // Extract car and cdr before mutably borrowing arena
214        let (car_val, cdr_val) = match arena.get(current) {
215            ValueData::Pair { car, cdr, .. } => (*car, *cdr),
216            _ => return Err("reverse: not a proper list".to_string()),
217        };
218
219        result = arena.cons(car_val, result);
220        current = cdr_val;
221    }
222}
223
224/// (append list ...) → list
225/// Concatenate lists
226pub fn append(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
227    if args.is_empty() {
228        return Ok(NIL_ID);
229    }
230
231    if args.len() == 1 {
232        return Ok(args[0]);
233    }
234
235    // Build result by appending each list
236    let mut result = args[args.len() - 1];
237
238    for i in (0..args.len() - 1).rev() {
239        let mut items = Vec::new();
240        let mut current = args[i];
241
242        // Collect items from this list
243        loop {
244            if current == NIL_ID {
245                break;
246            }
247
248            match arena.get(current) {
249                ValueData::Pair { car, cdr, .. } => {
250                    items.push(*car);
251                    current = *cdr;
252                }
253                _ => return Err("append: not a proper list".to_string()),
254            }
255        }
256
257        // Cons items onto result in reverse order
258        for &item in items.iter().rev() {
259            result = arena.cons(item, result);
260        }
261    }
262
263    Ok(result)
264}
265
266/// (list? obj) → boolean
267/// Check if object is a proper list
268pub fn list_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
269    if args.len() != 1 {
270        return Err("list? requires exactly 1 argument".to_string());
271    }
272
273    let mut current = args[0];
274    loop {
275        if current == NIL_ID {
276            return Ok(crate::scheme::arena::TRUE_ID);
277        }
278
279        match arena.get(current) {
280            ValueData::Pair { cdr, .. } => {
281                current = *cdr;
282            }
283            _ => return Ok(crate::scheme::arena::FALSE_ID),
284        }
285    }
286}
287
288/// (list-ref list k) → value
289/// Return the kth element of list (0-indexed)
290pub fn list_ref(arena: &Arena, args: &[ValueId]) -> ArenaResult {
291    if args.len() != 2 {
292        return Err("list-ref requires exactly 2 arguments".to_string());
293    }
294
295    let k = match arena.get(args[1]) {
296        ValueData::Integer(n) if *n >= 0 => *n as usize,
297        ValueData::Integer(_) => return Err("list-ref: index must be non-negative".to_string()),
298        _ => return Err("list-ref: index must be an integer".to_string()),
299    };
300
301    let mut current = args[0];
302    for _ in 0..k {
303        match arena.get(current) {
304            ValueData::Pair { cdr, .. } => {
305                current = *cdr;
306            }
307            _ => return Err("list-ref: index out of bounds".to_string()),
308        }
309    }
310
311    match arena.get(current) {
312        ValueData::Pair { car, .. } => Ok(*car),
313        _ => Err("list-ref: index out of bounds".to_string()),
314    }
315}
316
317// =============================================================================
318// Batch 2: Type Predicates (Phase 3)
319// =============================================================================
320
321/// (pair? obj) → boolean
322/// Check if object is a pair
323#[inline]
324pub fn pair_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
325    if args.len() != 1 {
326        return Err("pair? requires exactly 1 argument".to_string());
327    }
328
329    Ok(match arena.get(args[0]) {
330        ValueData::Pair { .. } => crate::scheme::arena::TRUE_ID,
331        _ => crate::scheme::arena::FALSE_ID,
332    })
333}
334
335/// (number? obj) → boolean
336/// Check if object is a number (integer or real)
337#[inline]
338pub fn number_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
339    if args.len() != 1 {
340        return Err("number? requires exactly 1 argument".to_string());
341    }
342
343    Ok(match arena.get(args[0]) {
344        ValueData::Integer(_) | ValueData::Real(_) => crate::scheme::arena::TRUE_ID,
345        _ => crate::scheme::arena::FALSE_ID,
346    })
347}
348
349/// (integer? obj) → boolean
350/// Check if object is an integer
351#[inline]
352pub fn integer_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
353    if args.len() != 1 {
354        return Err("integer? requires exactly 1 argument".to_string());
355    }
356
357    Ok(match arena.get(args[0]) {
358        ValueData::Integer(_) => crate::scheme::arena::TRUE_ID,
359        _ => crate::scheme::arena::FALSE_ID,
360    })
361}
362
363/// (real? obj) → boolean
364/// Check if object is a real number
365#[inline]
366pub fn real_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
367    if args.len() != 1 {
368        return Err("real? requires exactly 1 argument".to_string());
369    }
370
371    Ok(match arena.get(args[0]) {
372        ValueData::Real(_) => crate::scheme::arena::TRUE_ID,
373        _ => crate::scheme::arena::FALSE_ID,
374    })
375}
376
377/// (string? obj) → boolean
378/// Check if object is a string
379#[inline]
380pub fn string_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
381    if args.len() != 1 {
382        return Err("string? requires exactly 1 argument".to_string());
383    }
384
385    Ok(match arena.get(args[0]) {
386        ValueData::String(_) => crate::scheme::arena::TRUE_ID,
387        _ => crate::scheme::arena::FALSE_ID,
388    })
389}
390
391/// (symbol? obj) → boolean
392/// Check if object is a symbol
393#[inline]
394pub fn symbol_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
395    if args.len() != 1 {
396        return Err("symbol? requires exactly 1 argument".to_string());
397    }
398
399    Ok(match arena.get(args[0]) {
400        ValueData::Symbol(_) => crate::scheme::arena::TRUE_ID,
401        _ => crate::scheme::arena::FALSE_ID,
402    })
403}
404
405/// (char? obj) → boolean
406/// Check if object is a character
407#[inline]
408pub fn char_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
409    if args.len() != 1 {
410        return Err("char? requires exactly 1 argument".to_string());
411    }
412
413    Ok(match arena.get(args[0]) {
414        ValueData::Char(_) => crate::scheme::arena::TRUE_ID,
415        _ => crate::scheme::arena::FALSE_ID,
416    })
417}
418
419/// (boolean? obj) → boolean
420/// Check if object is a boolean
421#[inline]
422pub fn boolean_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
423    if args.len() != 1 {
424        return Err("boolean? requires exactly 1 argument".to_string());
425    }
426
427    Ok(match arena.get(args[0]) {
428        ValueData::Bool(_) => crate::scheme::arena::TRUE_ID,
429        _ => crate::scheme::arena::FALSE_ID,
430    })
431}
432
433/// (zero? num) → boolean
434/// Check if number is zero
435#[inline]
436pub fn zero_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
437    if args.len() != 1 {
438        return Err("zero? requires exactly 1 argument".to_string());
439    }
440
441    Ok(match arena.get(args[0]) {
442        ValueData::Integer(n) => {
443            if *n == 0 {
444                crate::scheme::arena::TRUE_ID
445            } else {
446                crate::scheme::arena::FALSE_ID
447            }
448        }
449        ValueData::Real(f) => {
450            if *f == 0.0 {
451                crate::scheme::arena::TRUE_ID
452            } else {
453                crate::scheme::arena::FALSE_ID
454            }
455        }
456        _ => return Err("zero?: not a number".to_string()),
457    })
458}
459
460/// (positive? num) → boolean
461/// Check if number is positive
462#[inline]
463pub fn positive_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
464    if args.len() != 1 {
465        return Err("positive? requires exactly 1 argument".to_string());
466    }
467
468    Ok(match arena.get(args[0]) {
469        ValueData::Integer(n) => {
470            if *n > 0 {
471                crate::scheme::arena::TRUE_ID
472            } else {
473                crate::scheme::arena::FALSE_ID
474            }
475        }
476        ValueData::Real(f) => {
477            if *f > 0.0 {
478                crate::scheme::arena::TRUE_ID
479            } else {
480                crate::scheme::arena::FALSE_ID
481            }
482        }
483        _ => return Err("positive?: not a number".to_string()),
484    })
485}
486
487/// (negative? num) → boolean
488/// Check if number is negative
489#[inline]
490pub fn negative_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
491    if args.len() != 1 {
492        return Err("negative? requires exactly 1 argument".to_string());
493    }
494
495    Ok(match arena.get(args[0]) {
496        ValueData::Integer(n) => {
497            if *n < 0 {
498                crate::scheme::arena::TRUE_ID
499            } else {
500                crate::scheme::arena::FALSE_ID
501            }
502        }
503        ValueData::Real(f) => {
504            if *f < 0.0 {
505                crate::scheme::arena::TRUE_ID
506            } else {
507                crate::scheme::arena::FALSE_ID
508            }
509        }
510        _ => return Err("negative?: not a number".to_string()),
511    })
512}
513
514/// (odd? n) → boolean
515/// Check if integer is odd
516#[inline]
517pub fn odd_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
518    if args.len() != 1 {
519        return Err("odd? requires exactly 1 argument".to_string());
520    }
521
522    match arena.get(args[0]) {
523        ValueData::Integer(n) => {
524            Ok(if n % 2 != 0 {
525                crate::scheme::arena::TRUE_ID
526            } else {
527                crate::scheme::arena::FALSE_ID
528            })
529        }
530        _ => Err("odd?: not an integer".to_string()),
531    }
532}
533
534/// (even? n) → boolean
535/// Check if integer is even
536#[inline]
537pub fn even_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
538    if args.len() != 1 {
539        return Err("even? requires exactly 1 argument".to_string());
540    }
541
542    match arena.get(args[0]) {
543        ValueData::Integer(n) => {
544            Ok(if n % 2 == 0 {
545                crate::scheme::arena::TRUE_ID
546            } else {
547                crate::scheme::arena::FALSE_ID
548            })
549        }
550        _ => Err("even?: not an integer".to_string()),
551    }
552}
553
554// =============================================================================
555// Batch 3: Arithmetic Operations (Phase 3)
556// =============================================================================
557
558/// Helper: Convert ValueId to numeric value (int or real)
559fn to_number(arena: &Arena, id: ValueId) -> Result<f64, String> {
560    match arena.get(id) {
561        ValueData::Integer(n) => Ok(*n as f64),
562        ValueData::Real(f) => Ok(*f),
563        ValueData::Quantity { magnitude, unit } => {
564            // Convert quantity to inches (canonical unit)
565            Ok(unit.to_inches(*magnitude))
566        }
567        _ => Err("not a number".to_string()),
568    }
569}
570
571/// Helper: Create result from f64 (preserve integer when possible)
572fn from_number(arena: &mut Arena, n: f64) -> ValueId {
573    if n.fract() == 0.0 && n.is_finite() && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
574        arena.int(n as i64)
575    } else {
576        arena.real(n)
577    }
578}
579
580/// (+ num ...) → number
581/// Add numbers or quantities (quantities must have compatible units)
582/// DSSSL: (+ quantity number) → quantity, (+ quantity quantity) → quantity
583pub fn add(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
584    if args.is_empty() {
585        return Ok(arena.int(0));
586    }
587
588    let mut sum = 0.0;
589    let mut all_ints = true;
590    let mut int_sum = 0i64;
591    let mut quantity_magnitude: Option<f64> = None;
592    let mut quantity_unit: Option<crate::scheme::value::Unit> = None;
593
594    for &arg in args {
595        match arena.get(arg) {
596            ValueData::Integer(n) => {
597                if quantity_magnitude.is_none() {
598                    int_sum = int_sum.wrapping_add(*n);
599                }
600                sum += *n as f64;
601            }
602            ValueData::Real(f) => {
603                all_ints = false;
604                sum += f;
605            }
606            ValueData::Quantity { magnitude, unit } => {
607                if let Some(_existing_unit) = quantity_unit {
608                    // OpenJade allows adding quantities with different units - just use first unit
609                    quantity_magnitude = Some(quantity_magnitude.unwrap() + magnitude);
610                } else {
611                    // First quantity: add any accumulated numeric sum to it
612                    quantity_magnitude = Some(*magnitude + sum);
613                    quantity_unit = Some(*unit);
614                    sum = 0.0; // Reset since we've incorporated it
615                }
616                all_ints = false;
617            }
618            _ => return Err("+: not a number".to_string()),
619        }
620    }
621
622    // If we had a quantity, add any remaining numeric sum to its magnitude
623    if let (Some(mag), Some(unit)) = (quantity_magnitude, quantity_unit) {
624        Ok(arena.alloc(ValueData::Quantity {
625            magnitude: mag + sum,
626            unit,
627        }))
628    } else {
629        Ok(if all_ints {
630            arena.int(int_sum)
631        } else {
632            arena.real(sum)
633        })
634    }
635}
636
637/// (- num ...) → number
638/// Subtract numbers
639pub fn subtract(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
640    if args.is_empty() {
641        return Err("-: requires at least 1 argument".to_string());
642    }
643
644    if args.len() == 1 {
645        // Negate
646        match arena.get(args[0]) {
647            ValueData::Integer(n) => return Ok(arena.int(-n)),
648            ValueData::Real(f) => return Ok(arena.real(-f)),
649            _ => return Err("-: not a number".to_string()),
650        }
651    }
652
653    let mut result = to_number(arena, args[0])?;
654    let mut all_ints = matches!(arena.get(args[0]), ValueData::Integer(_));
655
656    for &arg in &args[1..] {
657        result -= to_number(arena, arg)?;
658        if !matches!(arena.get(arg), ValueData::Integer(_)) {
659            all_ints = false;
660        }
661    }
662
663    Ok(if all_ints && result.fract() == 0.0 {
664        arena.int(result as i64)
665    } else {
666        arena.real(result)
667    })
668}
669
670/// (* num ...) → number
671/// Multiply numbers or scale a quantity by a number
672/// DSSSL: (* quantity number) → quantity (scales the quantity)
673pub fn multiply(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
674    if args.is_empty() {
675        return Ok(arena.int(1));
676    }
677
678    let mut product = 1.0;
679    let mut all_ints = true;
680    let mut int_product = 1i64;
681    let mut quantity_magnitude: Option<f64> = None;
682    let mut quantity_unit: Option<crate::scheme::value::Unit> = None;
683
684    for &arg in args {
685        match arena.get(arg) {
686            ValueData::Integer(n) => {
687                int_product = int_product.wrapping_mul(*n);
688                product *= *n as f64;
689            }
690            ValueData::Real(f) => {
691                all_ints = false;
692                product *= f;
693            }
694            ValueData::Quantity { magnitude, unit } => {
695                if quantity_unit.is_some() {
696                    return Err("*: cannot multiply two quantities".to_string());
697                }
698                quantity_magnitude = Some(*magnitude);
699                quantity_unit = Some(*unit);
700                all_ints = false;
701            }
702            _ => return Err("*: not a number".to_string()),
703        }
704    }
705
706    // If we had a quantity, scale it by the numeric product
707    if let (Some(mag), Some(unit)) = (quantity_magnitude, quantity_unit) {
708        Ok(arena.alloc(ValueData::Quantity {
709            magnitude: mag * product,
710            unit,
711        }))
712    } else {
713        Ok(if all_ints {
714            arena.int(int_product)
715        } else {
716            arena.real(product)
717        })
718    }
719}
720
721/// (/ num ...) → number
722/// Divide numbers (always returns real in R4RS)
723pub fn divide(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
724    if args.is_empty() {
725        return Err("/: requires at least 1 argument".to_string());
726    }
727
728    if args.len() == 1 {
729        // Reciprocal
730        let n = to_number(arena, args[0])?;
731        if n == 0.0 {
732            return Err("/: division by zero".to_string());
733        }
734        return Ok(arena.real(1.0 / n));
735    }
736
737    let mut result = to_number(arena, args[0])?;
738
739    for &arg in &args[1..] {
740        let n = to_number(arena, arg)?;
741        if n == 0.0 {
742            return Err("/: division by zero".to_string());
743        }
744        result /= n;
745    }
746
747    // Division always returns real in R4RS
748    Ok(arena.real(result))
749}
750
751/// (quotient n1 n2) → integer
752/// Integer division quotient
753pub fn quotient(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
754    if args.len() != 2 {
755        return Err("quotient requires exactly 2 arguments".to_string());
756    }
757
758    match (arena.get(args[0]), arena.get(args[1])) {
759        (ValueData::Integer(n1), ValueData::Integer(n2)) => {
760            if *n2 == 0 {
761                return Err("quotient: division by zero".to_string());
762            }
763            Ok(arena.int(n1 / n2))
764        }
765        _ => Err("quotient: arguments must be integers".to_string()),
766    }
767}
768
769/// (remainder n1 n2) → integer
770/// Integer division remainder
771pub fn remainder(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
772    if args.len() != 2 {
773        return Err("remainder requires exactly 2 arguments".to_string());
774    }
775
776    match (arena.get(args[0]), arena.get(args[1])) {
777        (ValueData::Integer(n1), ValueData::Integer(n2)) => {
778            if *n2 == 0 {
779                return Err("remainder: division by zero".to_string());
780            }
781            Ok(arena.int(n1 % n2))
782        }
783        _ => Err("remainder: arguments must be integers".to_string()),
784    }
785}
786
787/// (modulo n1 n2) → integer
788/// Modulo operation
789pub fn modulo(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
790    if args.len() != 2 {
791        return Err("modulo requires exactly 2 arguments".to_string());
792    }
793
794    match (arena.get(args[0]), arena.get(args[1])) {
795        (ValueData::Integer(n1), ValueData::Integer(n2)) => {
796            if *n2 == 0 {
797                return Err("modulo: division by zero".to_string());
798            }
799            Ok(arena.int(n1.rem_euclid(*n2)))
800        }
801        _ => Err("modulo: arguments must be integers".to_string()),
802    }
803}
804
805/// (= num1 num2 ...) → boolean
806/// Numeric equality
807pub fn num_eq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
808    if args.len() < 2 {
809        return Err("=: requires at least 2 arguments".to_string());
810    }
811
812    let first = to_number(arena, args[0])?;
813    for &arg in &args[1..] {
814        if to_number(arena, arg)? != first {
815            return Ok(crate::scheme::arena::FALSE_ID);
816        }
817    }
818    Ok(crate::scheme::arena::TRUE_ID)
819}
820
821/// (< num1 num2 ...) → boolean
822/// Numeric less-than
823pub fn num_lt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
824    if args.len() < 2 {
825        return Err("<: requires at least 2 arguments".to_string());
826    }
827
828    let mut prev = to_number(arena, args[0])?;
829    for &arg in &args[1..] {
830        let curr = to_number(arena, arg)?;
831        if prev >= curr {
832            return Ok(crate::scheme::arena::FALSE_ID);
833        }
834        prev = curr;
835    }
836    Ok(crate::scheme::arena::TRUE_ID)
837}
838
839/// (> num1 num2 ...) → boolean
840/// Numeric greater-than
841pub fn num_gt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
842    if args.len() < 2 {
843        return Err(">: requires at least 2 arguments".to_string());
844    }
845
846    let mut prev = to_number(arena, args[0])?;
847    for &arg in &args[1..] {
848        let curr = to_number(arena, arg)?;
849        if prev <= curr {
850            return Ok(crate::scheme::arena::FALSE_ID);
851        }
852        prev = curr;
853    }
854    Ok(crate::scheme::arena::TRUE_ID)
855}
856
857/// (<= num1 num2 ...) → boolean
858/// Numeric less-than-or-equal
859pub fn num_le(arena: &Arena, args: &[ValueId]) -> ArenaResult {
860    if args.len() < 2 {
861        return Err("<=: requires at least 2 arguments".to_string());
862    }
863
864    let mut prev = to_number(arena, args[0])?;
865    for &arg in &args[1..] {
866        let curr = to_number(arena, arg)?;
867        if prev > curr {
868            return Ok(crate::scheme::arena::FALSE_ID);
869        }
870        prev = curr;
871    }
872    Ok(crate::scheme::arena::TRUE_ID)
873}
874
875/// (>= num1 num2 ...) → boolean
876/// Numeric greater-than-or-equal
877pub fn num_ge(arena: &Arena, args: &[ValueId]) -> ArenaResult {
878    if args.len() < 2 {
879        return Err(">=: requires at least 2 arguments".to_string());
880    }
881
882    let mut prev = to_number(arena, args[0])?;
883    for &arg in &args[1..] {
884        let curr = to_number(arena, arg)?;
885        if prev < curr {
886            return Ok(crate::scheme::arena::FALSE_ID);
887        }
888        prev = curr;
889    }
890    Ok(crate::scheme::arena::TRUE_ID)
891}
892
893/// (abs num) → number
894/// Absolute value
895pub fn abs(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
896    if args.len() != 1 {
897        return Err("abs requires exactly 1 argument".to_string());
898    }
899
900    match arena.get(args[0]) {
901        ValueData::Integer(n) => Ok(arena.int(n.abs())),
902        ValueData::Real(f) => Ok(arena.real(f.abs())),
903        _ => Err("abs: not a number".to_string()),
904    }
905}
906
907/// (min num ...) → number
908/// Minimum value
909pub fn min(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
910    if args.is_empty() {
911        return Err("min requires at least 1 argument".to_string());
912    }
913
914    let mut min_val = to_number(arena, args[0])?;
915    let mut all_ints = matches!(arena.get(args[0]), ValueData::Integer(_));
916
917    for &arg in &args[1..] {
918        let val = to_number(arena, arg)?;
919        if val < min_val {
920            min_val = val;
921        }
922        if !matches!(arena.get(arg), ValueData::Integer(_)) {
923            all_ints = false;
924        }
925    }
926
927    Ok(if all_ints {
928        arena.int(min_val as i64)
929    } else {
930        arena.real(min_val)
931    })
932}
933
934/// (max num ...) → number
935/// Maximum value
936pub fn max(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
937    if args.is_empty() {
938        return Err("max requires at least 1 argument".to_string());
939    }
940
941    let mut max_val = to_number(arena, args[0])?;
942    let mut all_ints = matches!(arena.get(args[0]), ValueData::Integer(_));
943
944    for &arg in &args[1..] {
945        let val = to_number(arena, arg)?;
946        if val > max_val {
947            max_val = val;
948        }
949        if !matches!(arena.get(arg), ValueData::Integer(_)) {
950            all_ints = false;
951        }
952    }
953
954    Ok(if all_ints {
955        arena.int(max_val as i64)
956    } else {
957        arena.real(max_val)
958    })
959}
960
961/// (floor num) → integer
962/// Round down
963pub fn floor(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
964    if args.len() != 1 {
965        return Err("floor requires exactly 1 argument".to_string());
966    }
967
968    let n = to_number(arena, args[0])?;
969    Ok(arena.int(n.floor() as i64))
970}
971
972/// (ceiling num) → integer
973/// Round up
974pub fn ceiling(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
975    if args.len() != 1 {
976        return Err("ceiling requires exactly 1 argument".to_string());
977    }
978
979    let n = to_number(arena, args[0])?;
980    Ok(arena.int(n.ceil() as i64))
981}
982
983/// (truncate num) → integer
984/// Round toward zero
985pub fn truncate(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
986    if args.len() != 1 {
987        return Err("truncate requires exactly 1 argument".to_string());
988    }
989
990    let n = to_number(arena, args[0])?;
991    Ok(arena.int(n.trunc() as i64))
992}
993
994/// (round num) → integer
995/// Round to nearest
996pub fn round(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
997    if args.len() != 1 {
998        return Err("round requires exactly 1 argument".to_string());
999    }
1000
1001    let n = to_number(arena, args[0])?;
1002    Ok(arena.int(n.round() as i64))
1003}
1004
1005/// (sqrt num) → number
1006/// Square root
1007pub fn sqrt(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1008    if args.len() != 1 {
1009        return Err("sqrt requires exactly 1 argument".to_string());
1010    }
1011
1012    let n = to_number(arena, args[0])?;
1013    Ok(arena.real(n.sqrt()))
1014}
1015
1016/// (sin num) → number
1017/// Sine
1018pub fn sin(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1019    if args.len() != 1 {
1020        return Err("sin requires exactly 1 argument".to_string());
1021    }
1022
1023    let n = to_number(arena, args[0])?;
1024    Ok(arena.real(n.sin()))
1025}
1026
1027/// (cos num) → number
1028/// Cosine
1029pub fn cos(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1030    if args.len() != 1 {
1031        return Err("cos requires exactly 1 argument".to_string());
1032    }
1033
1034    let n = to_number(arena, args[0])?;
1035    Ok(arena.real(n.cos()))
1036}
1037
1038/// (tan num) → number
1039/// Tangent
1040pub fn tan(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1041    if args.len() != 1 {
1042        return Err("tan requires exactly 1 argument".to_string());
1043    }
1044
1045    let n = to_number(arena, args[0])?;
1046    Ok(arena.real(n.tan()))
1047}
1048
1049/// (asin num) → number
1050/// Arcsine
1051pub fn asin(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1052    if args.len() != 1 {
1053        return Err("asin requires exactly 1 argument".to_string());
1054    }
1055
1056    let n = to_number(arena, args[0])?;
1057    Ok(arena.real(n.asin()))
1058}
1059
1060/// (acos num) → number
1061/// Arccosine
1062pub fn acos(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1063    if args.len() != 1 {
1064        return Err("acos requires exactly 1 argument".to_string());
1065    }
1066
1067    let n = to_number(arena, args[0])?;
1068    Ok(arena.real(n.acos()))
1069}
1070
1071/// (atan num) → number
1072/// Arctangent
1073pub fn atan(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1074    if args.len() != 1 {
1075        return Err("atan requires exactly 1 argument".to_string());
1076    }
1077
1078    let n = to_number(arena, args[0])?;
1079    Ok(arena.real(n.atan()))
1080}
1081
1082/// (exp num) → number
1083/// Exponential (e^x)
1084pub fn exp(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1085    if args.len() != 1 {
1086        return Err("exp requires exactly 1 argument".to_string());
1087    }
1088
1089    let n = to_number(arena, args[0])?;
1090    Ok(arena.real(n.exp()))
1091}
1092
1093/// (log num) → number
1094/// Natural logarithm
1095pub fn log(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1096    if args.len() != 1 {
1097        return Err("log requires exactly 1 argument".to_string());
1098    }
1099
1100    let n = to_number(arena, args[0])?;
1101    Ok(arena.real(n.ln()))
1102}
1103
1104/// (expt base exponent) → number
1105/// Exponentiation
1106pub fn expt(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1107    if args.len() != 2 {
1108        return Err("expt requires exactly 2 arguments".to_string());
1109    }
1110
1111    let base = to_number(arena, args[0])?;
1112    let exp = to_number(arena, args[1])?;
1113    Ok(arena.real(base.powf(exp)))
1114}
1115
1116// =============================================================================
1117// Batch 4: String Operations (Phase 3)
1118// =============================================================================
1119
1120/// (string-length str) → integer
1121/// Return length of string
1122pub fn string_length(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1123    if args.len() != 1 {
1124        return Err("string-length requires exactly 1 argument".to_string());
1125    }
1126
1127    match arena.get(args[0]) {
1128        // IMPORTANT: Must count characters, not bytes (for UTF-8 compatibility)
1129        // string-ref uses chars().nth() which indexes by character
1130        ValueData::String(s) => Ok(arena.int(s.chars().count() as i64)),
1131        _ => Err("string-length: not a string".to_string()),
1132    }
1133}
1134
1135/// (string-ref str k) → char
1136/// Return character at position k
1137pub fn string_ref(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1138    if args.len() != 2 {
1139        return Err("string-ref requires exactly 2 arguments".to_string());
1140    }
1141
1142    let s = match arena.get(args[0]) {
1143        ValueData::String(s) => s,
1144        _ => return Err("string-ref: first argument must be a string".to_string()),
1145    };
1146
1147    let k = match arena.get(args[1]) {
1148        ValueData::Integer(n) if *n >= 0 => *n as usize,
1149        ValueData::Integer(_) => return Err("string-ref: index must be non-negative".to_string()),
1150        _ => return Err("string-ref: second argument must be an integer".to_string()),
1151    };
1152
1153    match s.chars().nth(k) {
1154        Some(c) => Ok(arena.char(c)),
1155        None => Err("string-ref: index out of bounds".to_string()),
1156    }
1157}
1158
1159/// (substring str start end) → string
1160/// Extract substring from start to end
1161pub fn substring(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1162    if args.len() != 3 {
1163        return Err("substring requires exactly 3 arguments".to_string());
1164    }
1165
1166    // OpenJade: stringData() accepts both strings and symbols
1167    let s = get_string_data(arena, args[0], "substring")?;
1168
1169    let start = match arena.get(args[1]) {
1170        ValueData::Integer(n) if *n >= 0 => *n as usize,
1171        ValueData::Integer(_) => return Err("substring: start must be non-negative".to_string()),
1172        _ => return Err("substring: start must be an integer".to_string()),
1173    };
1174
1175    let end = match arena.get(args[2]) {
1176        ValueData::Integer(n) if *n >= 0 => *n as usize,
1177        ValueData::Integer(_) => return Err("substring: end must be non-negative".to_string()),
1178        _ => return Err("substring: end must be an integer".to_string()),
1179    };
1180
1181    let chars: Vec<char> = s.chars().collect();
1182    let len = chars.len();
1183
1184    // OpenJade clamps indices to string bounds instead of erroring
1185    // This allows (substring "" 0 1) to return "" instead of failing
1186    let clamped_start = start.min(len);
1187    let clamped_end = end.min(len);
1188
1189    if clamped_start > clamped_end {
1190        return Err("substring: start must be <= end".to_string());
1191    }
1192
1193    let result: String = chars[clamped_start..clamped_end].iter().collect();
1194    Ok(arena.string(result))
1195}
1196
1197/// (string-append str ...) → string
1198/// Concatenate strings
1199pub fn string_append(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1200    let mut result = String::new();
1201
1202    for &arg in args {
1203        match arena.get(arg) {
1204            ValueData::String(s) => result.push_str(s),
1205            // OpenJade treats unspecified as empty string in string contexts
1206            ValueData::Unspecified => {},  // Append nothing (empty string)
1207            _ => return Err("string-append: all arguments must be strings".to_string()),
1208        }
1209    }
1210
1211    Ok(arena.string(result))
1212}
1213
1214/// Helper: Extract string data from String or Symbol (like OpenJade's stringData())
1215fn get_string_data<'a>(arena: &'a Arena, id: ValueId, prim_name: &str) -> Result<std::borrow::Cow<'a, str>, String> {
1216    match arena.get(id) {
1217        ValueData::String(s) => Ok(std::borrow::Cow::Borrowed(s.as_str())),
1218        ValueData::Symbol(s) => Ok(std::borrow::Cow::Borrowed(s.as_ref())),
1219        _ => Err(format!("{}: not a string or symbol", prim_name)),
1220    }
1221}
1222
1223/// (string=? str1 str2 ...) → boolean
1224/// String equality (case-sensitive)
1225pub fn string_eq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1226    if args.len() < 2 {
1227        return Err("string=?: requires at least 2 arguments".to_string());
1228    }
1229
1230    let first = get_string_data(arena, args[0], "string=?")?;
1231
1232    for &arg in &args[1..] {
1233        let s = get_string_data(arena, arg, "string=?")?;
1234        if s != first {
1235            return Ok(crate::scheme::arena::FALSE_ID);
1236        }
1237    }
1238
1239    Ok(crate::scheme::arena::TRUE_ID)
1240}
1241
1242/// (string<? str1 str2 ...) → boolean
1243/// String less-than (case-sensitive)
1244pub fn string_lt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1245    if args.len() < 2 {
1246        return Err("string<?: requires at least 2 arguments".to_string());
1247    }
1248
1249    let mut prev = get_string_data(arena, args[0], "string<?")?.into_owned();
1250
1251    for &arg in &args[1..] {
1252        let s = get_string_data(arena, arg, "string<?")?;
1253        if prev.as_str() >= s.as_ref() {
1254            return Ok(crate::scheme::arena::FALSE_ID);
1255        }
1256        prev = s.into_owned();
1257    }
1258
1259    Ok(crate::scheme::arena::TRUE_ID)
1260}
1261
1262/// (string>? str1 str2 ...) → boolean
1263/// String greater-than (case-sensitive)
1264pub fn string_gt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1265    if args.len() < 2 {
1266        return Err("string>?: requires at least 2 arguments".to_string());
1267    }
1268
1269    let mut prev = get_string_data(arena, args[0], "string>?")?.into_owned();
1270
1271    for &arg in &args[1..] {
1272        let s = get_string_data(arena, arg, "string>?")?;
1273        if prev.as_str() <= s.as_ref() {
1274            return Ok(crate::scheme::arena::FALSE_ID);
1275        }
1276        prev = s.into_owned();
1277    }
1278
1279    Ok(crate::scheme::arena::TRUE_ID)
1280}
1281
1282/// (string<=? str1 str2 ...) → boolean
1283/// String less-than-or-equal (case-sensitive)
1284pub fn string_le(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1285    if args.len() < 2 {
1286        return Err("string<=?: requires at least 2 arguments".to_string());
1287    }
1288
1289    let mut prev = get_string_data(arena, args[0], "string<=?")?.into_owned();
1290
1291    for &arg in &args[1..] {
1292        let s = get_string_data(arena, arg, "string<=?")?;
1293        if prev.as_str() > s.as_ref() {
1294            return Ok(crate::scheme::arena::FALSE_ID);
1295        }
1296        prev = s.into_owned();
1297    }
1298
1299    Ok(crate::scheme::arena::TRUE_ID)
1300}
1301
1302/// (string>=? str1 str2 ...) → boolean
1303/// String greater-than-or-equal (case-sensitive)
1304pub fn string_ge(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1305    if args.len() < 2 {
1306        return Err("string>=?: requires at least 2 arguments".to_string());
1307    }
1308
1309    let mut prev = get_string_data(arena, args[0], "string>=?")?.into_owned();
1310
1311    for &arg in &args[1..] {
1312        let s = get_string_data(arena, arg, "string>=?")?;
1313        if prev.as_str() < s.as_ref() {
1314            return Ok(crate::scheme::arena::FALSE_ID);
1315        }
1316        prev = s.into_owned();
1317    }
1318
1319    Ok(crate::scheme::arena::TRUE_ID)
1320}
1321
1322/// (string-ci=? str1 str2 ...) → boolean
1323/// String equality (case-insensitive)
1324pub fn string_ci_eq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1325    if args.len() < 2 {
1326        return Err("string-ci=?: requires at least 2 arguments".to_string());
1327    }
1328
1329    let first = get_string_data(arena, args[0], "string-ci=?")?.to_lowercase();
1330
1331    for &arg in &args[1..] {
1332        let s = get_string_data(arena, arg, "string-ci=?")?;
1333        if s.to_lowercase() != first {
1334            return Ok(crate::scheme::arena::FALSE_ID);
1335        }
1336    }
1337
1338    Ok(crate::scheme::arena::TRUE_ID)
1339}
1340
1341/// (string-ci<? str1 str2 ...) → boolean
1342/// String less-than (case-insensitive)
1343pub fn string_ci_lt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1344    if args.len() < 2 {
1345        return Err("string-ci<?: requires at least 2 arguments".to_string());
1346    }
1347
1348    let mut prev = get_string_data(arena, args[0], "string-ci<?")?.to_lowercase();
1349
1350    for &arg in &args[1..] {
1351        let s = get_string_data(arena, arg, "string-ci<?")?;
1352        let curr = s.to_lowercase();
1353        if prev >= curr {
1354            return Ok(crate::scheme::arena::FALSE_ID);
1355        }
1356        prev = curr;
1357    }
1358
1359    Ok(crate::scheme::arena::TRUE_ID)
1360}
1361
1362/// (string-ci>? str1 str2 ...) → boolean
1363/// String greater-than (case-insensitive)
1364pub fn string_ci_gt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1365    if args.len() < 2 {
1366        return Err("string-ci>?: requires at least 2 arguments".to_string());
1367    }
1368
1369    let mut prev = get_string_data(arena, args[0], "string-ci>?")?.to_lowercase();
1370
1371    for &arg in &args[1..] {
1372        let s = get_string_data(arena, arg, "string-ci>?")?;
1373        let curr = s.to_lowercase();
1374        if prev <= curr {
1375            return Ok(crate::scheme::arena::FALSE_ID);
1376        }
1377        prev = curr;
1378    }
1379
1380    Ok(crate::scheme::arena::TRUE_ID)
1381}
1382
1383/// (string-ci<=? str1 str2 ...) → boolean
1384/// String less-than-or-equal (case-insensitive)
1385pub fn string_ci_le(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1386    if args.len() < 2 {
1387        return Err("string-ci<=?: requires at least 2 arguments".to_string());
1388    }
1389
1390    let mut prev = get_string_data(arena, args[0], "string-ci<=?")?.to_lowercase();
1391
1392    for &arg in &args[1..] {
1393        let s = get_string_data(arena, arg, "string-ci<=?")?;
1394        let curr = s.to_lowercase();
1395        if prev > curr {
1396            return Ok(crate::scheme::arena::FALSE_ID);
1397        }
1398        prev = curr;
1399    }
1400
1401    Ok(crate::scheme::arena::TRUE_ID)
1402}
1403
1404/// (string-ci>=? str1 str2 ...) → boolean
1405/// String greater-than-or-equal (case-insensitive)
1406pub fn string_ci_ge(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1407    if args.len() < 2 {
1408        return Err("string-ci>=?: requires at least 2 arguments".to_string());
1409    }
1410
1411    let mut prev = get_string_data(arena, args[0], "string-ci>=?")?.to_lowercase();
1412
1413    for &arg in &args[1..] {
1414        let s = get_string_data(arena, arg, "string-ci>=?")?;
1415        let curr = s.to_lowercase();
1416        if prev < curr {
1417            return Ok(crate::scheme::arena::FALSE_ID);
1418        }
1419        prev = curr;
1420    }
1421
1422    Ok(crate::scheme::arena::TRUE_ID)
1423}
1424
1425// ============================================================================
1426// Phase 3 Batch 5: Character Operations (19 primitives)
1427// ============================================================================
1428
1429/// (char=? char1 char2) → boolean
1430/// Character equality (case-sensitive)
1431#[inline]
1432pub fn char_eq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1433    if args.len() != 2 {
1434        return Err("char=? requires exactly 2 arguments".to_string());
1435    }
1436
1437    let c1 = match arena.get(args[0]) {
1438        ValueData::Char(c) => *c,
1439        _ => return Err("char=?: not a character".to_string()),
1440    };
1441
1442    let c2 = match arena.get(args[1]) {
1443        ValueData::Char(c) => *c,
1444        _ => return Err("char=?: not a character".to_string()),
1445    };
1446
1447    Ok(if c1 == c2 {
1448        crate::scheme::arena::TRUE_ID
1449    } else {
1450        crate::scheme::arena::FALSE_ID
1451    })
1452}
1453
1454/// (char<? char1 char2) → boolean
1455/// Character less-than (case-sensitive)
1456#[inline]
1457pub fn char_lt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1458    if args.len() != 2 {
1459        return Err("char<? requires exactly 2 arguments".to_string());
1460    }
1461
1462    let c1 = match arena.get(args[0]) {
1463        ValueData::Char(c) => *c,
1464        _ => return Err("char<?: not a character".to_string()),
1465    };
1466
1467    let c2 = match arena.get(args[1]) {
1468        ValueData::Char(c) => *c,
1469        _ => return Err("char<?: not a character".to_string()),
1470    };
1471
1472    Ok(if c1 < c2 {
1473        crate::scheme::arena::TRUE_ID
1474    } else {
1475        crate::scheme::arena::FALSE_ID
1476    })
1477}
1478
1479/// (char>? char1 char2) → boolean
1480/// Character greater-than (case-sensitive)
1481#[inline]
1482pub fn char_gt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1483    if args.len() != 2 {
1484        return Err("char>? requires exactly 2 arguments".to_string());
1485    }
1486
1487    let c1 = match arena.get(args[0]) {
1488        ValueData::Char(c) => *c,
1489        _ => return Err("char>?: not a character".to_string()),
1490    };
1491
1492    let c2 = match arena.get(args[1]) {
1493        ValueData::Char(c) => *c,
1494        _ => return Err("char>?: not a character".to_string()),
1495    };
1496
1497    Ok(if c1 > c2 {
1498        crate::scheme::arena::TRUE_ID
1499    } else {
1500        crate::scheme::arena::FALSE_ID
1501    })
1502}
1503
1504/// (char<=? char1 char2) → boolean
1505/// Character less-than-or-equal (case-sensitive)
1506#[inline]
1507pub fn char_le(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1508    if args.len() != 2 {
1509        return Err("char<=? requires exactly 2 arguments".to_string());
1510    }
1511
1512    let c1 = match arena.get(args[0]) {
1513        ValueData::Char(c) => *c,
1514        _ => return Err("char<=?: not a character".to_string()),
1515    };
1516
1517    let c2 = match arena.get(args[1]) {
1518        ValueData::Char(c) => *c,
1519        _ => return Err("char<=?: not a character".to_string()),
1520    };
1521
1522    Ok(if c1 <= c2 {
1523        crate::scheme::arena::TRUE_ID
1524    } else {
1525        crate::scheme::arena::FALSE_ID
1526    })
1527}
1528
1529/// (char>=? char1 char2) → boolean
1530/// Character greater-than-or-equal (case-sensitive)
1531#[inline]
1532pub fn char_ge(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1533    if args.len() != 2 {
1534        return Err("char>=? requires exactly 2 arguments".to_string());
1535    }
1536
1537    let c1 = match arena.get(args[0]) {
1538        ValueData::Char(c) => *c,
1539        _ => return Err("char>=?: not a character".to_string()),
1540    };
1541
1542    let c2 = match arena.get(args[1]) {
1543        ValueData::Char(c) => *c,
1544        _ => return Err("char>=?: not a character".to_string()),
1545    };
1546
1547    Ok(if c1 >= c2 {
1548        crate::scheme::arena::TRUE_ID
1549    } else {
1550        crate::scheme::arena::FALSE_ID
1551    })
1552}
1553
1554/// (char-ci=? char1 char2) → boolean
1555/// Character equality (case-insensitive)
1556#[inline]
1557pub fn char_ci_eq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1558    if args.len() != 2 {
1559        return Err("char-ci=? requires exactly 2 arguments".to_string());
1560    }
1561
1562    let c1 = match arena.get(args[0]) {
1563        ValueData::Char(c) => c.to_ascii_lowercase(),
1564        _ => return Err("char-ci=?: not a character".to_string()),
1565    };
1566
1567    let c2 = match arena.get(args[1]) {
1568        ValueData::Char(c) => c.to_ascii_lowercase(),
1569        _ => return Err("char-ci=?: not a character".to_string()),
1570    };
1571
1572    Ok(if c1 == c2 {
1573        crate::scheme::arena::TRUE_ID
1574    } else {
1575        crate::scheme::arena::FALSE_ID
1576    })
1577}
1578
1579/// (char-ci<? char1 char2) → boolean
1580/// Character less-than (case-insensitive)
1581#[inline]
1582pub fn char_ci_lt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1583    if args.len() != 2 {
1584        return Err("char-ci<? requires exactly 2 arguments".to_string());
1585    }
1586
1587    let c1 = match arena.get(args[0]) {
1588        ValueData::Char(c) => c.to_ascii_lowercase(),
1589        _ => return Err("char-ci<?: not a character".to_string()),
1590    };
1591
1592    let c2 = match arena.get(args[1]) {
1593        ValueData::Char(c) => c.to_ascii_lowercase(),
1594        _ => return Err("char-ci<?: not a character".to_string()),
1595    };
1596
1597    Ok(if c1 < c2 {
1598        crate::scheme::arena::TRUE_ID
1599    } else {
1600        crate::scheme::arena::FALSE_ID
1601    })
1602}
1603
1604/// (char-ci>? char1 char2) → boolean
1605/// Character greater-than (case-insensitive)
1606#[inline]
1607pub fn char_ci_gt(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1608    if args.len() != 2 {
1609        return Err("char-ci>? requires exactly 2 arguments".to_string());
1610    }
1611
1612    let c1 = match arena.get(args[0]) {
1613        ValueData::Char(c) => c.to_ascii_lowercase(),
1614        _ => return Err("char-ci>?: not a character".to_string()),
1615    };
1616
1617    let c2 = match arena.get(args[1]) {
1618        ValueData::Char(c) => c.to_ascii_lowercase(),
1619        _ => return Err("char-ci>?: not a character".to_string()),
1620    };
1621
1622    Ok(if c1 > c2 {
1623        crate::scheme::arena::TRUE_ID
1624    } else {
1625        crate::scheme::arena::FALSE_ID
1626    })
1627}
1628
1629/// (char-ci<=? char1 char2) → boolean
1630/// Character less-than-or-equal (case-insensitive)
1631#[inline]
1632pub fn char_ci_le(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1633    if args.len() != 2 {
1634        return Err("char-ci<=? requires exactly 2 arguments".to_string());
1635    }
1636
1637    let c1 = match arena.get(args[0]) {
1638        ValueData::Char(c) => c.to_ascii_lowercase(),
1639        _ => return Err("char-ci<=?: not a character".to_string()),
1640    };
1641
1642    let c2 = match arena.get(args[1]) {
1643        ValueData::Char(c) => c.to_ascii_lowercase(),
1644        _ => return Err("char-ci<=?: not a character".to_string()),
1645    };
1646
1647    Ok(if c1 <= c2 {
1648        crate::scheme::arena::TRUE_ID
1649    } else {
1650        crate::scheme::arena::FALSE_ID
1651    })
1652}
1653
1654/// (char-ci>=? char1 char2) → boolean
1655/// Character greater-than-or-equal (case-insensitive)
1656#[inline]
1657pub fn char_ci_ge(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1658    if args.len() != 2 {
1659        return Err("char-ci>=? requires exactly 2 arguments".to_string());
1660    }
1661
1662    let c1 = match arena.get(args[0]) {
1663        ValueData::Char(c) => c.to_ascii_lowercase(),
1664        _ => return Err("char-ci>=?: not a character".to_string()),
1665    };
1666
1667    let c2 = match arena.get(args[1]) {
1668        ValueData::Char(c) => c.to_ascii_lowercase(),
1669        _ => return Err("char-ci>=?: not a character".to_string()),
1670    };
1671
1672    Ok(if c1 >= c2 {
1673        crate::scheme::arena::TRUE_ID
1674    } else {
1675        crate::scheme::arena::FALSE_ID
1676    })
1677}
1678
1679/// (char-upcase char) → char
1680/// Convert character to uppercase
1681pub fn char_upcase(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1682    if args.len() != 1 {
1683        return Err("char-upcase requires exactly 1 argument".to_string());
1684    }
1685
1686    match arena.get(args[0]) {
1687        ValueData::Char(c) => Ok(arena.char(c.to_ascii_uppercase())),
1688        _ => Err("char-upcase: not a character".to_string()),
1689    }
1690}
1691
1692/// (char-downcase char) → char
1693/// Convert character to lowercase
1694pub fn char_downcase(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1695    if args.len() != 1 {
1696        return Err("char-downcase requires exactly 1 argument".to_string());
1697    }
1698
1699    match arena.get(args[0]) {
1700        ValueData::Char(c) => Ok(arena.char(c.to_ascii_lowercase())),
1701        _ => Err("char-downcase: not a character".to_string()),
1702    }
1703}
1704
1705/// (char-alphabetic? char) → boolean
1706/// Test if character is alphabetic
1707#[inline]
1708pub fn char_alphabetic_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1709    if args.len() != 1 {
1710        return Err("char-alphabetic? requires exactly 1 argument".to_string());
1711    }
1712
1713    match arena.get(args[0]) {
1714        ValueData::Char(c) => Ok(if c.is_alphabetic() {
1715            crate::scheme::arena::TRUE_ID
1716        } else {
1717            crate::scheme::arena::FALSE_ID
1718        }),
1719        _ => Err("char-alphabetic?: not a character".to_string()),
1720    }
1721}
1722
1723/// (char-numeric? char) → boolean
1724/// Test if character is numeric
1725#[inline]
1726pub fn char_numeric_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1727    if args.len() != 1 {
1728        return Err("char-numeric? requires exactly 1 argument".to_string());
1729    }
1730
1731    match arena.get(args[0]) {
1732        ValueData::Char(c) => Ok(if c.is_numeric() {
1733            crate::scheme::arena::TRUE_ID
1734        } else {
1735            crate::scheme::arena::FALSE_ID
1736        }),
1737        _ => Err("char-numeric?: not a character".to_string()),
1738    }
1739}
1740
1741/// (char-whitespace? char) → boolean
1742/// Test if character is whitespace
1743#[inline]
1744pub fn char_whitespace_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1745    if args.len() != 1 {
1746        return Err("char-whitespace? requires exactly 1 argument".to_string());
1747    }
1748
1749    match arena.get(args[0]) {
1750        ValueData::Char(c) => Ok(if c.is_whitespace() {
1751            crate::scheme::arena::TRUE_ID
1752        } else {
1753            crate::scheme::arena::FALSE_ID
1754        }),
1755        _ => Err("char-whitespace?: not a character".to_string()),
1756    }
1757}
1758
1759/// (char->integer char) → integer
1760/// Convert character to its Unicode code point
1761pub fn char_to_integer(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1762    if args.len() != 1 {
1763        return Err("char->integer requires exactly 1 argument".to_string());
1764    }
1765
1766    match arena.get(args[0]) {
1767        ValueData::Char(c) => Ok(arena.int(*c as i64)),
1768        _ => Err("char->integer: not a character".to_string()),
1769    }
1770}
1771
1772/// (integer->char n) → char
1773/// Convert integer code point to character
1774pub fn integer_to_char(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1775    if args.len() != 1 {
1776        return Err("integer->char requires exactly 1 argument".to_string());
1777    }
1778
1779    match arena.get(args[0]) {
1780        ValueData::Integer(n) => {
1781            if *n < 0 || *n > 0x10FFFF {
1782                return Err(format!("integer->char: invalid code point: {}", n));
1783            }
1784            match char::from_u32(*n as u32) {
1785                Some(c) => Ok(arena.char(c)),
1786                None => Err(format!("integer->char: invalid Unicode code point: {}", n)),
1787            }
1788        }
1789        _ => Err("integer->char: not an integer".to_string()),
1790    }
1791}
1792
1793/// (char-property char) → string
1794/// DSSSL extension - returns dummy value
1795pub fn char_property(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1796    if args.len() != 1 {
1797        return Err("char-property requires exactly 1 argument".to_string());
1798    }
1799
1800    match arena.get(args[0]) {
1801        ValueData::Char(_) => Ok(arena.string("unknown".to_string())),
1802        _ => Err("char-property: not a character".to_string()),
1803    }
1804}
1805
1806/// (char-script-case char) → symbol
1807/// DSSSL extension - returns dummy value
1808pub fn char_script_case(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1809    if args.len() != 1 {
1810        return Err("char-script-case requires exactly 1 argument".to_string());
1811    }
1812
1813    match arena.get(args[0]) {
1814        ValueData::Char(_) => Ok(arena.symbol(std::rc::Rc::from("unknown"))),
1815        _ => Err("char-script-case: not a character".to_string()),
1816    }
1817}
1818
1819// ============================================================================
1820// Phase 3 Batch 6: Symbol/Keyword Operations (5 primitives)
1821// ============================================================================
1822
1823/// (symbol->string symbol) → string
1824/// Convert symbol to its string name
1825pub fn symbol_to_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1826    if args.len() != 1 {
1827        return Err("symbol->string requires exactly 1 argument".to_string());
1828    }
1829
1830    match arena.get(args[0]) {
1831        ValueData::Symbol(s) => Ok(arena.string(s.to_string())),
1832        _ => Err("symbol->string: not a symbol".to_string()),
1833    }
1834}
1835
1836/// (string->symbol string) → symbol
1837/// Convert string to a symbol
1838pub fn string_to_symbol(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1839    if args.len() != 1 {
1840        return Err("string->symbol requires exactly 1 argument".to_string());
1841    }
1842
1843    match arena.get(args[0]) {
1844        ValueData::String(s) => Ok(arena.symbol(std::rc::Rc::from(s.as_str()))),
1845        _ => Err("string->symbol: not a string".to_string()),
1846    }
1847}
1848
1849/// (keyword? obj) → boolean
1850/// Test if object is a keyword
1851#[inline]
1852pub fn keyword_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1853    if args.len() != 1 {
1854        return Err("keyword? requires exactly 1 argument".to_string());
1855    }
1856
1857    Ok(match arena.get(args[0]) {
1858        ValueData::Keyword(_) => crate::scheme::arena::TRUE_ID,
1859        _ => crate::scheme::arena::FALSE_ID,
1860    })
1861}
1862
1863/// (keyword->string keyword) → string
1864/// Convert keyword to its string name
1865pub fn keyword_to_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1866    if args.len() != 1 {
1867        return Err("keyword->string requires exactly 1 argument".to_string());
1868    }
1869
1870    match arena.get(args[0]) {
1871        ValueData::Keyword(k) => Ok(arena.string(k.to_string())),
1872        _ => Err("keyword->string: not a keyword".to_string()),
1873    }
1874}
1875
1876/// (string->keyword string) → keyword
1877/// Convert string to a keyword
1878pub fn string_to_keyword(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
1879    if args.len() != 1 {
1880        return Err("string->keyword requires exactly 1 argument".to_string());
1881    }
1882
1883    match arena.get(args[0]) {
1884        ValueData::String(s) => Ok(arena.keyword(std::rc::Rc::from(s.as_str()))),
1885        _ => Err("string->keyword: not a string".to_string()),
1886    }
1887}
1888
1889// ============================================================================
1890// Phase 3 Batch 7: List Utilities (6 primitives)
1891// ============================================================================
1892
1893/// Helper: eq? comparison for arena values
1894fn eq(arena: &Arena, a: ValueId, b: ValueId) -> bool {
1895    // Fast path: same ID means same object
1896    if a == b {
1897        return true;
1898    }
1899
1900    // eq? compares integers, booleans, and characters by value
1901    // (special case in R4RS - not pure identity for these types)
1902    match (arena.get(a), arena.get(b)) {
1903        (ValueData::Integer(n1), ValueData::Integer(n2)) => n1 == n2,
1904        (ValueData::Char(c1), ValueData::Char(c2)) => c1 == c2,
1905        (ValueData::Bool(b1), ValueData::Bool(b2)) => b1 == b2,
1906        (ValueData::Symbol(s1), ValueData::Symbol(s2)) => s1 == s2,
1907        _ => false,  // For all other types, eq? is identity
1908    }
1909}
1910
1911/// Helper: eqv? comparison for arena values
1912fn eqv(arena: &Arena, a: ValueId, b: ValueId) -> bool {
1913    // Fast path: same ID
1914    if a == b {
1915        return true;
1916    }
1917
1918    // eqv? compares numbers, characters, booleans, and symbols by value
1919    match (arena.get(a), arena.get(b)) {
1920        (ValueData::Integer(n1), ValueData::Integer(n2)) => n1 == n2,
1921        (ValueData::Real(f1), ValueData::Real(f2)) => f1 == f2,
1922        (ValueData::Char(c1), ValueData::Char(c2)) => c1 == c2,
1923        (ValueData::Bool(b1), ValueData::Bool(b2)) => b1 == b2,
1924        (ValueData::Symbol(s1), ValueData::Symbol(s2)) => s1 == s2,
1925        _ => false,
1926    }
1927}
1928
1929/// (memq obj list) → list or #f
1930/// Search list for obj using eq? comparison
1931pub fn memq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1932    if args.len() != 2 {
1933        return Err("memq requires exactly 2 arguments".to_string());
1934    }
1935
1936    let obj = args[0];
1937    let mut current = args[1];
1938
1939    loop {
1940        if current == crate::scheme::arena::NIL_ID {
1941            return Ok(crate::scheme::arena::FALSE_ID);
1942        }
1943
1944        match arena.get(current) {
1945            ValueData::Pair { car, cdr, .. } => {
1946                if eq(arena, obj, *car) {
1947                    return Ok(current);
1948                }
1949                current = *cdr;
1950            }
1951            _ => return Err("memq: not a proper list".to_string()),
1952        }
1953    }
1954}
1955
1956/// (memv obj list) → list or #f
1957/// Search list for obj using eqv? comparison
1958pub fn memv(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1959    if args.len() != 2 {
1960        return Err("memv requires exactly 2 arguments".to_string());
1961    }
1962
1963    let obj = args[0];
1964    let mut current = args[1];
1965
1966    loop {
1967        if current == crate::scheme::arena::NIL_ID {
1968            return Ok(crate::scheme::arena::FALSE_ID);
1969        }
1970
1971        match arena.get(current) {
1972            ValueData::Pair { car, cdr, .. } => {
1973                if eqv(arena, obj, *car) {
1974                    return Ok(current);
1975                }
1976                current = *cdr;
1977            }
1978            _ => return Err("memv: not a proper list".to_string()),
1979        }
1980    }
1981}
1982
1983/// (member obj list) → list or #f
1984/// Search list for obj using equal? comparison
1985pub fn member(arena: &Arena, args: &[ValueId]) -> ArenaResult {
1986    if args.len() != 2 {
1987        return Err("member requires exactly 2 arguments".to_string());
1988    }
1989
1990    let obj = args[0];
1991    let mut current = args[1];
1992
1993    loop {
1994        if current == crate::scheme::arena::NIL_ID {
1995            return Ok(crate::scheme::arena::FALSE_ID);
1996        }
1997
1998        match arena.get(current) {
1999            ValueData::Pair { car, cdr, .. } => {
2000                if equal_impl(arena, obj, *car) {
2001                    return Ok(current);
2002                }
2003                current = *cdr;
2004            }
2005            _ => return Err("member: not a proper list".to_string()),
2006        }
2007    }
2008}
2009
2010/// (assq obj alist) → pair or #f
2011/// Search association list for pair with car eq? to obj
2012pub fn assq(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2013    if args.len() != 2 {
2014        return Err("assq requires exactly 2 arguments".to_string());
2015    }
2016
2017    let obj = args[0];
2018    let mut current = args[1];
2019
2020    loop {
2021        if current == crate::scheme::arena::NIL_ID {
2022            return Ok(crate::scheme::arena::FALSE_ID);
2023        }
2024
2025        match arena.get(current) {
2026            ValueData::Pair { car, cdr, .. } => {
2027                // Check if car is a pair
2028                if let ValueData::Pair { car: inner_car, .. } = arena.get(*car) {
2029                    if eq(arena, obj, *inner_car) {
2030                        return Ok(*car);
2031                    }
2032                }
2033                current = *cdr;
2034            }
2035            _ => return Err("assq: not a proper list".to_string()),
2036        }
2037    }
2038}
2039
2040/// (assv obj alist) → pair or #f
2041/// Search association list for pair with car eqv? to obj
2042pub fn assv(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2043    if args.len() != 2 {
2044        return Err("assv requires exactly 2 arguments".to_string());
2045    }
2046
2047    let obj = args[0];
2048    let mut current = args[1];
2049
2050    loop {
2051        if current == crate::scheme::arena::NIL_ID {
2052            return Ok(crate::scheme::arena::FALSE_ID);
2053        }
2054
2055        match arena.get(current) {
2056            ValueData::Pair { car, cdr, .. } => {
2057                // Check if car is a pair
2058                if let ValueData::Pair { car: inner_car, .. } = arena.get(*car) {
2059                    if eqv(arena, obj, *inner_car) {
2060                        return Ok(*car);
2061                    }
2062                }
2063                current = *cdr;
2064            }
2065            _ => return Err("assv: not a proper list".to_string()),
2066        }
2067    }
2068}
2069
2070/// (assoc obj alist) → pair or #f
2071/// Search association list for pair with car equal? to obj
2072pub fn assoc(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2073    if args.len() != 2 {
2074        return Err("assoc requires exactly 2 arguments".to_string());
2075    }
2076
2077    let obj = args[0];
2078    let mut current = args[1];
2079
2080    loop {
2081        if current == crate::scheme::arena::NIL_ID {
2082            return Ok(crate::scheme::arena::FALSE_ID);
2083        }
2084
2085        match arena.get(current) {
2086            ValueData::Pair { car, cdr, .. } => {
2087                // Check if car is a pair
2088                if let ValueData::Pair { car: inner_car, .. } = arena.get(*car) {
2089                    if equal_impl(arena, obj, *inner_car) {
2090                        return Ok(*car);
2091                    }
2092                }
2093                current = *cdr;
2094            }
2095            _ => return Err("assoc: not a proper list".to_string()),
2096        }
2097    }
2098}
2099
2100// ============================================================================
2101// Phase 3 Batch 8: Logic and List Accessors (6 primitives)
2102// ============================================================================
2103
2104/// (not obj) → boolean
2105/// Logical not - returns #t if obj is false, #f otherwise
2106#[inline]
2107pub fn not(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
2108    if args.len() != 1 {
2109        return Err("not requires exactly 1 argument".to_string());
2110    }
2111
2112    let val = args[0];
2113    // In Scheme, only #f is false; everything else is true
2114    Ok(if val == crate::scheme::arena::FALSE_ID {
2115        crate::scheme::arena::TRUE_ID
2116    } else {
2117        crate::scheme::arena::FALSE_ID
2118    })
2119}
2120
2121/// (eq? obj1 obj2) → boolean
2122/// Test for object identity (pointer equality)
2123#[inline]
2124pub fn eq_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2125    if args.len() != 2 {
2126        return Err("eq? requires exactly 2 arguments".to_string());
2127    }
2128
2129    Ok(if eq(arena, args[0], args[1]) {
2130        crate::scheme::arena::TRUE_ID
2131    } else {
2132        crate::scheme::arena::FALSE_ID
2133    })
2134}
2135
2136/// (eqv? obj1 obj2) → boolean
2137/// Test for equivalence (numbers and chars by value, others by identity)
2138#[inline]
2139pub fn eqv_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2140    if args.len() != 2 {
2141        return Err("eqv? requires exactly 2 arguments".to_string());
2142    }
2143
2144    Ok(if eqv(arena, args[0], args[1]) {
2145        crate::scheme::arena::TRUE_ID
2146    } else {
2147        crate::scheme::arena::FALSE_ID
2148    })
2149}
2150
2151/// (caar list) → value
2152/// Equivalent to (car (car list))
2153#[inline]
2154pub fn caar(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2155    if args.len() != 1 {
2156        return Err("caar requires exactly 1 argument".to_string());
2157    }
2158
2159    let car1 = car(arena, args)?;
2160    car(arena, &[car1])
2161}
2162
2163/// (cdar list) → value
2164/// Equivalent to (cdr (car list))
2165#[inline]
2166pub fn cdar(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2167    if args.len() != 1 {
2168        return Err("cdar requires exactly 1 argument".to_string());
2169    }
2170
2171    let car_val = car(arena, args)?;
2172    cdr(arena, &[car_val])
2173}
2174
2175/// (cddr list) → value
2176/// Equivalent to (cdr (cdr list))
2177#[inline]
2178pub fn cddr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2179    if args.len() != 1 {
2180        return Err("cddr requires exactly 1 argument".to_string());
2181    }
2182
2183    let cdr1 = cdr(arena, args)?;
2184    cdr(arena, &[cdr1])
2185}
2186
2187// Phase 3 Batch 9: Extended List Accessors (7 primitives)
2188
2189/// (caaar list) → value
2190/// Equivalent to (car (car (car list)))
2191#[inline]
2192pub fn caaar(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2193    if args.len() != 1 {
2194        return Err("caaar requires exactly 1 argument".to_string());
2195    }
2196    let car1 = car(arena, args)?;
2197    let car2 = car(arena, &[car1])?;
2198    car(arena, &[car2])
2199}
2200
2201/// (caadr list) → value
2202/// Equivalent to (car (car (cdr list)))
2203#[inline]
2204pub fn caadr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2205    if args.len() != 1 {
2206        return Err("caadr requires exactly 1 argument".to_string());
2207    }
2208    let cdr1 = cdr(arena, args)?;
2209    let car2 = car(arena, &[cdr1])?;
2210    car(arena, &[car2])
2211}
2212
2213/// (cadar list) → value
2214/// Equivalent to (car (cdr (car list)))
2215#[inline]
2216pub fn cadar(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2217    if args.len() != 1 {
2218        return Err("cadar requires exactly 1 argument".to_string());
2219    }
2220    let car1 = car(arena, args)?;
2221    let cdr2 = cdr(arena, &[car1])?;
2222    car(arena, &[cdr2])
2223}
2224
2225/// (cdaar list) → value
2226/// Equivalent to (cdr (car (car list)))
2227#[inline]
2228pub fn cdaar(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2229    if args.len() != 1 {
2230        return Err("cdaar requires exactly 1 argument".to_string());
2231    }
2232    let car1 = car(arena, args)?;
2233    let car2 = car(arena, &[car1])?;
2234    cdr(arena, &[car2])
2235}
2236
2237/// (cdadr list) → value
2238/// Equivalent to (cdr (car (cdr list)))
2239#[inline]
2240pub fn cdadr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2241    if args.len() != 1 {
2242        return Err("cdadr requires exactly 1 argument".to_string());
2243    }
2244    let cdr1 = cdr(arena, args)?;
2245    let car2 = car(arena, &[cdr1])?;
2246    cdr(arena, &[car2])
2247}
2248
2249/// (cddar list) → value
2250/// Equivalent to (cdr (cdr (car list)))
2251#[inline]
2252pub fn cddar(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2253    if args.len() != 1 {
2254        return Err("cddar requires exactly 1 argument".to_string());
2255    }
2256    let car1 = car(arena, args)?;
2257    let cdr2 = cdr(arena, &[car1])?;
2258    cdr(arena, &[cdr2])
2259}
2260
2261/// (cdddr list) → value
2262/// Equivalent to (cdr (cdr (cdr list)))
2263#[inline]
2264pub fn cdddr(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2265    if args.len() != 1 {
2266        return Err("cdddr requires exactly 1 argument".to_string());
2267    }
2268    let cdr1 = cdr(arena, args)?;
2269    let cdr2 = cdr(arena, &[cdr1])?;
2270    cdr(arena, &[cdr2])
2271}
2272
2273// Phase 3 Batch 10: Vector Operations (8 primitives)
2274
2275/// (vector obj ...) → vector
2276/// Create a vector from the given arguments
2277#[inline]
2278pub fn vector(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2279    Ok(arena.vector(args.to_vec()))
2280}
2281
2282/// (make-vector k [fill]) → vector
2283/// Create a vector of length k, optionally filled with fill value
2284#[inline]
2285pub fn make_vector(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2286    if args.is_empty() || args.len() > 2 {
2287        return Err("make-vector requires 1 or 2 arguments".to_string());
2288    }
2289
2290    match arena.get(args[0]) {
2291        ValueData::Integer(k) if *k >= 0 => {
2292            let size = *k as usize;
2293            let fill = if args.len() == 2 {
2294                args[1]
2295            } else {
2296                crate::scheme::arena::UNSPECIFIED_ID
2297            };
2298            let elements = vec![fill; size];
2299            Ok(arena.vector(elements))
2300        }
2301        ValueData::Integer(_) => Err("make-vector: size must be non-negative".to_string()),
2302        _ => Err("make-vector: first argument must be an integer".to_string()),
2303    }
2304}
2305
2306/// (vector-length vec) → integer
2307/// Return the length of the vector
2308#[inline]
2309pub fn vector_length(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2310    if args.len() != 1 {
2311        return Err("vector-length requires exactly 1 argument".to_string());
2312    }
2313
2314    match arena.get(args[0]) {
2315        ValueData::Vector(elements) => Ok(arena.int(elements.len() as i64)),
2316        _ => Err("vector-length: argument must be a vector".to_string()),
2317    }
2318}
2319
2320/// (vector-ref vec k) → value
2321/// Return the element at index k
2322#[inline]
2323pub fn vector_ref(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2324    if args.len() != 2 {
2325        return Err("vector-ref requires exactly 2 arguments".to_string());
2326    }
2327
2328    let vec_id = args[0];
2329    match arena.get(vec_id) {
2330        ValueData::Vector(elements) => {
2331            match arena.get(args[1]) {
2332                ValueData::Integer(k) if *k >= 0 => {
2333                    let index = *k as usize;
2334                    if index < elements.len() {
2335                        Ok(elements[index])
2336                    } else {
2337                        Err(format!("vector-ref: index {} out of bounds (length {})", index, elements.len()))
2338                    }
2339                }
2340                ValueData::Integer(_) => Err("vector-ref: index must be non-negative".to_string()),
2341                _ => Err("vector-ref: second argument must be an integer".to_string()),
2342            }
2343        }
2344        _ => Err("vector-ref: first argument must be a vector".to_string()),
2345    }
2346}
2347
2348/// (vector-set! vec k obj) → unspecified
2349/// Set the element at index k to obj
2350/// NOTE: This mutates the vector in place (returns unspecified)
2351#[inline]
2352pub fn vector_set(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2353    if args.len() != 3 {
2354        return Err("vector-set! requires exactly 3 arguments".to_string());
2355    }
2356
2357    let vec_id = args[0];
2358    let new_val = args[2];
2359
2360    match arena.get(args[1]) {
2361        ValueData::Integer(k) if *k >= 0 => {
2362            let index = *k as usize;
2363            match arena.get_mut(vec_id) {
2364                ValueData::Vector(ref mut elements) => {
2365                    if index < elements.len() {
2366                        elements[index] = new_val;
2367                        Ok(crate::scheme::arena::UNSPECIFIED_ID)
2368                    } else {
2369                        Err(format!("vector-set!: index {} out of bounds (length {})", index, elements.len()))
2370                    }
2371                }
2372                _ => Err("vector-set!: first argument must be a vector".to_string()),
2373            }
2374        }
2375        ValueData::Integer(_) => Err("vector-set!: index must be non-negative".to_string()),
2376        _ => Err("vector-set!: second argument must be an integer".to_string()),
2377    }
2378}
2379
2380/// (vector->list vec) → list
2381/// Convert a vector to a list
2382#[inline]
2383pub fn vector_to_list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2384    if args.len() != 1 {
2385        return Err("vector->list requires exactly 1 argument".to_string());
2386    }
2387
2388    // Clone elements to avoid borrowing arena while mutating it
2389    let elements = match arena.get(args[0]) {
2390        ValueData::Vector(elements) => elements.clone(),
2391        _ => return Err("vector->list: argument must be a vector".to_string()),
2392    };
2393
2394    // Build list from vector elements (in reverse order for efficiency)
2395    let mut result = crate::scheme::arena::NIL_ID;
2396    for &elem in elements.iter().rev() {
2397        result = arena.cons(elem, result);
2398    }
2399    Ok(result)
2400}
2401
2402/// (list->vector list) → vector
2403/// Convert a list to a vector
2404#[inline]
2405pub fn list_to_vector(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2406    if args.len() != 1 {
2407        return Err("list->vector requires exactly 1 argument".to_string());
2408    }
2409
2410    // Collect list elements
2411    let mut elements = Vec::new();
2412    let mut current = args[0];
2413
2414    loop {
2415        match arena.get(current) {
2416            ValueData::Nil => break,
2417            ValueData::Pair { car, cdr, .. } => {
2418                elements.push(*car);
2419                current = *cdr;
2420            }
2421            _ => return Err("list->vector: argument must be a proper list".to_string()),
2422        }
2423    }
2424
2425    Ok(arena.vector(elements))
2426}
2427
2428/// (vector-fill! vec fill) → unspecified
2429/// Fill all elements of the vector with the fill value
2430#[inline]
2431pub fn vector_fill(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2432    if args.len() != 2 {
2433        return Err("vector-fill! requires exactly 2 arguments".to_string());
2434    }
2435
2436    let vec_id = args[0];
2437    let fill = args[1];
2438
2439    match arena.get_mut(vec_id) {
2440        ValueData::Vector(ref mut elements) => {
2441            for elem in elements.iter_mut() {
2442                *elem = fill;
2443            }
2444            Ok(crate::scheme::arena::UNSPECIFIED_ID)
2445        }
2446        _ => Err("vector-fill!: first argument must be a vector".to_string()),
2447    }
2448}
2449
2450// Phase 3 Batch 11: Additional Type Predicates & Utilities (5 primitives)
2451
2452/// (vector? obj) → boolean
2453/// Returns #t if obj is a vector, #f otherwise
2454#[inline]
2455pub fn vector_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2456    if args.len() != 1 {
2457        return Err("vector? requires exactly 1 argument".to_string());
2458    }
2459
2460    match arena.get(args[0]) {
2461        ValueData::Vector(_) => Ok(crate::scheme::arena::TRUE_ID),
2462        _ => Ok(crate::scheme::arena::FALSE_ID),
2463    }
2464}
2465
2466/// (procedure? obj) → boolean
2467/// Returns #t if obj is a procedure, #f otherwise
2468#[inline]
2469pub fn procedure_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2470    if args.len() != 1 {
2471        return Err("procedure? requires exactly 1 argument".to_string());
2472    }
2473
2474    match arena.get(args[0]) {
2475        ValueData::Procedure(_) => Ok(crate::scheme::arena::TRUE_ID),
2476        _ => Ok(crate::scheme::arena::FALSE_ID),
2477    }
2478}
2479
2480/// (set-car! pair obj) → unspecified
2481/// Mutate the car of a pair
2482#[inline]
2483pub fn set_car(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2484    if args.len() != 2 {
2485        return Err("set-car! requires exactly 2 arguments".to_string());
2486    }
2487
2488    let pair_id = args[0];
2489    let new_car = args[1];
2490
2491    match arena.get_mut(pair_id) {
2492        ValueData::Pair { car, .. } => {
2493            *car = new_car;
2494            Ok(crate::scheme::arena::UNSPECIFIED_ID)
2495        }
2496        _ => Err("set-car!: first argument must be a pair".to_string()),
2497    }
2498}
2499
2500/// (set-cdr! pair obj) → unspecified
2501/// Mutate the cdr of a pair
2502#[inline]
2503pub fn set_cdr(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2504    if args.len() != 2 {
2505        return Err("set-cdr! requires exactly 2 arguments".to_string());
2506    }
2507
2508    let pair_id = args[0];
2509    let new_cdr = args[1];
2510
2511    match arena.get_mut(pair_id) {
2512        ValueData::Pair { cdr, .. } => {
2513            *cdr = new_cdr;
2514            Ok(crate::scheme::arena::UNSPECIFIED_ID)
2515        }
2516        _ => Err("set-cdr!: first argument must be a pair".to_string()),
2517    }
2518}
2519
2520/// (list-tail list k) → value
2521/// Returns the sublist of list obtained by omitting the first k elements
2522#[inline]
2523pub fn list_tail(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2524    if args.len() != 2 {
2525        return Err("list-tail requires exactly 2 arguments".to_string());
2526    }
2527
2528    let k = match arena.get(args[1]) {
2529        ValueData::Integer(n) if *n >= 0 => *n as usize,
2530        ValueData::Integer(_) => return Err("list-tail: index must be non-negative".to_string()),
2531        _ => return Err("list-tail: second argument must be an integer".to_string()),
2532    };
2533
2534    let mut current = args[0];
2535    for _ in 0..k {
2536        match arena.get(current) {
2537            ValueData::Pair { cdr, .. } => {
2538                current = *cdr;
2539            }
2540            ValueData::Nil => return Err("list-tail: list too short".to_string()),
2541            _ => return Err("list-tail: argument must be a list".to_string()),
2542        }
2543    }
2544
2545    Ok(current)
2546}
2547
2548// Phase 3 Batch 12: String Utilities (6 primitives)
2549
2550/// (string-upcase str) → string
2551/// Convert string to uppercase
2552#[inline]
2553pub fn string_upcase(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2554    if args.len() != 1 {
2555        return Err("string-upcase requires exactly 1 argument".to_string());
2556    }
2557
2558    match arena.get(args[0]) {
2559        ValueData::String(s) => Ok(arena.string(s.to_uppercase())),
2560        _ => Err("string-upcase: argument must be a string".to_string()),
2561    }
2562}
2563
2564/// (string-downcase str) → string
2565/// Convert string to lowercase
2566#[inline]
2567pub fn string_downcase(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2568    if args.len() != 1 {
2569        return Err("string-downcase requires exactly 1 argument".to_string());
2570    }
2571
2572    match arena.get(args[0]) {
2573        ValueData::String(s) => Ok(arena.string(s.to_lowercase())),
2574        _ => Err("string-downcase: argument must be a string".to_string()),
2575    }
2576}
2577
2578/// (case-fold-down str) → string
2579/// DSSSL alias for string-downcase - convert string to lowercase
2580#[inline]
2581pub fn case_fold_down(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2582    string_downcase(arena, args)
2583}
2584
2585/// (string-index str char-or-string) → integer
2586/// Find the index of the first occurrence of a character/substring in a string
2587/// Returns the 0-based index, or -1 if not found
2588#[inline]
2589pub fn string_index(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2590    if args.len() != 2 {
2591        return Err("string-index requires exactly 2 arguments".to_string());
2592    }
2593
2594    let s = match arena.get(args[0]) {
2595        ValueData::String(s) => s.clone(),
2596        _ => return Err("string-index: first argument must be a string".to_string()),
2597    };
2598
2599    // Second argument can be either a character or a string
2600    let search_str = match arena.get(args[1]) {
2601        ValueData::Char(c) => c.to_string(),
2602        ValueData::String(search) => search.clone(),
2603        _ => return Err("string-index: second argument must be a character or string".to_string()),
2604    };
2605
2606    // Search for the substring/character
2607    match s.find(search_str.as_str()) {
2608        Some(idx) => {
2609            // Convert byte index to character index
2610            let char_idx = s[..idx].chars().count();
2611            Ok(arena.int(char_idx as i64))
2612        }
2613        None => Ok(arena.int(-1)),  // Return -1 when not found (DocBook convention)
2614    }
2615}
2616
2617/// (string->number str) → number or #f
2618/// Parse a string as a number
2619#[inline]
2620pub fn string_to_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2621    if args.len() != 1 {
2622        return Err("string->number requires exactly 1 argument".to_string());
2623    }
2624
2625    match arena.get(args[0]) {
2626        ValueData::String(s) => {
2627            // Try integer first
2628            if let Ok(n) = s.parse::<i64>() {
2629                return Ok(arena.int(n));
2630            }
2631            // Try float
2632            if let Ok(f) = s.parse::<f64>() {
2633                return Ok(arena.real(f));
2634            }
2635            // Parse failed - return #f
2636            Ok(crate::scheme::arena::FALSE_ID)
2637        }
2638        _ => Err("string->number: argument must be a string".to_string()),
2639    }
2640}
2641
2642/// (number->string num) → string
2643/// Convert a number to a string
2644#[inline]
2645pub fn number_to_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2646    if args.len() != 1 {
2647        return Err("number->string requires exactly 1 argument".to_string());
2648    }
2649
2650    match arena.get(args[0]) {
2651        ValueData::Integer(n) => Ok(arena.string(n.to_string())),
2652        ValueData::Real(f) => Ok(arena.string(f.to_string())),
2653        _ => Err("number->string: argument must be a number".to_string()),
2654    }
2655}
2656
2657/// (string->list str) → list
2658/// Convert a string to a list of characters
2659#[inline]
2660pub fn string_to_list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2661    if args.len() != 1 {
2662        return Err("string->list requires exactly 1 argument".to_string());
2663    }
2664
2665    // Clone the string to avoid borrow checker issues
2666    let s = match arena.get(args[0]) {
2667        ValueData::String(s) => s.clone(),
2668        _ => return Err("string->list: argument must be a string".to_string()),
2669    };
2670
2671    let mut result = crate::scheme::arena::NIL_ID;
2672    for c in s.chars().rev() {
2673        let char_id = arena.char(c);
2674        result = arena.cons(char_id, result);
2675    }
2676    Ok(result)
2677}
2678
2679/// (list->string list) → string
2680/// Convert a list of characters to a string
2681#[inline]
2682pub fn list_to_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2683    if args.len() != 1 {
2684        return Err("list->string requires exactly 1 argument".to_string());
2685    }
2686
2687    let mut chars = Vec::new();
2688    let mut current = args[0];
2689
2690    loop {
2691        match arena.get(current) {
2692            ValueData::Nil => break,
2693            ValueData::Pair { car, cdr, .. } => {
2694                match arena.get(*car) {
2695                    ValueData::Char(c) => {
2696                        chars.push(*c);
2697                        current = *cdr;
2698                    }
2699                    _ => return Err("list->string: list must contain only characters".to_string()),
2700                }
2701            }
2702            _ => return Err("list->string: argument must be a proper list".to_string()),
2703        }
2704    }
2705
2706    let s: String = chars.into_iter().collect();
2707    Ok(arena.string(s))
2708}
2709
2710// Phase 3 Batch 13: Miscellaneous Utilities (7 primitives)
2711
2712/// (gcd n1 n2 ...) → integer
2713/// Compute greatest common divisor
2714#[inline]
2715pub fn gcd(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2716    if args.is_empty() {
2717        return Ok(arena.int(0));
2718    }
2719
2720    fn gcd_two(a: i64, b: i64) -> i64 {
2721        let (mut a, mut b) = (a.abs(), b.abs());
2722        while b != 0 {
2723            let temp = b;
2724            b = a % b;
2725            a = temp;
2726        }
2727        a
2728    }
2729
2730    let mut result = 0i64;
2731    for &arg in args {
2732        match arena.get(arg) {
2733            ValueData::Integer(n) => {
2734                result = gcd_two(result, *n);
2735            }
2736            _ => return Err("gcd: all arguments must be integers".to_string()),
2737        }
2738    }
2739
2740    Ok(arena.int(result))
2741}
2742
2743/// (lcm n1 n2 ...) → integer
2744/// Compute least common multiple
2745#[inline]
2746pub fn lcm(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2747    if args.is_empty() {
2748        return Ok(arena.int(1));
2749    }
2750
2751    fn gcd_two(a: i64, b: i64) -> i64 {
2752        let (mut a, mut b) = (a.abs(), b.abs());
2753        while b != 0 {
2754            let temp = b;
2755            b = a % b;
2756            a = temp;
2757        }
2758        a
2759    }
2760
2761    fn lcm_two(a: i64, b: i64) -> i64 {
2762        if a == 0 || b == 0 {
2763            return 0;
2764        }
2765        (a.abs() / gcd_two(a, b)) * b.abs()
2766    }
2767
2768    let mut result = 1i64;
2769    for &arg in args {
2770        match arena.get(arg) {
2771            ValueData::Integer(n) => {
2772                result = lcm_two(result, *n);
2773            }
2774            _ => return Err("lcm: all arguments must be integers".to_string()),
2775        }
2776    }
2777
2778    Ok(arena.int(result))
2779}
2780
2781/// (exact->inexact num) → real
2782/// Convert exact number to inexact (integer to real)
2783#[inline]
2784pub fn exact_to_inexact(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2785    if args.len() != 1 {
2786        return Err("exact->inexact requires exactly 1 argument".to_string());
2787    }
2788
2789    match arena.get(args[0]) {
2790        ValueData::Integer(n) => Ok(arena.real(*n as f64)),
2791        ValueData::Real(_) => Ok(args[0]), // Already inexact
2792        _ => Err("exact->inexact: argument must be a number".to_string()),
2793    }
2794}
2795
2796/// (inexact->exact num) → integer
2797/// Convert inexact number to exact (real to integer)
2798#[inline]
2799pub fn inexact_to_exact(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2800    if args.len() != 1 {
2801        return Err("inexact->exact requires exactly 1 argument".to_string());
2802    }
2803
2804    match arena.get(args[0]) {
2805        ValueData::Real(f) => Ok(arena.int(f.round() as i64)),
2806        ValueData::Integer(_) => Ok(args[0]), // Already exact
2807        _ => Err("inexact->exact: argument must be a number".to_string()),
2808    }
2809}
2810
2811/// (make-string k [char]) → string
2812/// Create a string of length k filled with char (or space)
2813#[inline]
2814pub fn make_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2815    if args.is_empty() || args.len() > 2 {
2816        return Err("make-string requires 1 or 2 arguments".to_string());
2817    }
2818
2819    let k = match arena.get(args[0]) {
2820        ValueData::Integer(n) if *n >= 0 => *n as usize,
2821        ValueData::Integer(_) => return Err("make-string: length must be non-negative".to_string()),
2822        _ => return Err("make-string: first argument must be an integer".to_string()),
2823    };
2824
2825    let fill_char = if args.len() == 2 {
2826        match arena.get(args[1]) {
2827            ValueData::Char(c) => *c,
2828            _ => return Err("make-string: second argument must be a character".to_string()),
2829        }
2830    } else {
2831        ' '
2832    };
2833
2834    let s: String = std::iter::repeat(fill_char).take(k).collect();
2835    Ok(arena.string(s))
2836}
2837
2838/// (string char ...) → string
2839/// Construct a string from characters
2840#[inline]
2841pub fn string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2842    let mut chars = Vec::new();
2843
2844    for &arg in args {
2845        match arena.get(arg) {
2846            ValueData::Char(c) => chars.push(*c),
2847            _ => return Err("string: all arguments must be characters".to_string()),
2848        }
2849    }
2850
2851    let s: String = chars.into_iter().collect();
2852    Ok(arena.string(s))
2853}
2854
2855/// (reverse! list) → list
2856/// Destructively reverse a list
2857#[inline]
2858pub fn reverse_bang(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2859    if args.len() != 1 {
2860        return Err("reverse! requires exactly 1 argument".to_string());
2861    }
2862
2863    // Collect pairs first to avoid borrow issues
2864    let mut pairs = Vec::new();
2865    let mut current = args[0];
2866
2867    loop {
2868        match arena.get(current) {
2869            ValueData::Nil => break,
2870            ValueData::Pair { car, cdr, .. } => {
2871                pairs.push((current, *car));
2872                current = *cdr;
2873            }
2874            _ => return Err("reverse!: argument must be a list".to_string()),
2875        }
2876    }
2877
2878    if pairs.is_empty() {
2879        return Ok(crate::scheme::arena::NIL_ID);
2880    }
2881
2882    // Reverse by mutating cdr pointers
2883    let mut prev = crate::scheme::arena::NIL_ID;
2884    for &(pair_id, _car_val) in &pairs {
2885        match arena.get_mut(pair_id) {
2886            ValueData::Pair { cdr, .. } => {
2887                *cdr = prev;
2888                prev = pair_id;
2889            }
2890            _ => unreachable!(),
2891        }
2892    }
2893
2894    Ok(prev)
2895}
2896
2897// =============================================================================
2898// Batch 14: Additional String and Character Operations (5 primitives)
2899// =============================================================================
2900
2901/// string-set! - mutate string at index
2902/// Note: Strings in arena are immutable, so this creates a new string
2903pub fn string_set(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2904    if args.len() != 3 {
2905        return Err("string-set!: expected 3 arguments".to_string());
2906    }
2907
2908    let s = match arena.get(args[0]) {
2909        ValueData::String(s) => s.clone(),
2910        _ => return Err("string-set!: first argument must be a string".to_string()),
2911    };
2912
2913    let k = match arena.get(args[1]) {
2914        ValueData::Integer(n) => *n as usize,
2915        _ => return Err("string-set!: second argument must be an integer".to_string()),
2916    };
2917
2918    let c = match arena.get(args[2]) {
2919        ValueData::Char(c) => *c,
2920        _ => return Err("string-set!: third argument must be a character".to_string()),
2921    };
2922
2923    if k >= s.len() {
2924        return Err(format!("string-set!: index {} out of bounds for string of length {}", k, s.len()));
2925    }
2926
2927    let mut chars: Vec<char> = s.chars().collect();
2928    chars[k] = c;
2929    let new_string: String = chars.into_iter().collect();
2930
2931    Ok(arena.string(new_string))
2932}
2933
2934/// string-copy - copy a string
2935pub fn string_copy(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2936    if args.len() != 1 {
2937        return Err("string-copy: expected 1 argument".to_string());
2938    }
2939
2940    let s = match arena.get(args[0]) {
2941        ValueData::String(s) => s.clone(),
2942        _ => return Err("string-copy: argument must be a string".to_string()),
2943    };
2944
2945    Ok(arena.string(s))
2946}
2947
2948/// string-fill! - fill string with character
2949/// Note: Strings in arena are immutable, so this creates a new string
2950pub fn string_fill(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
2951    if args.len() != 2 {
2952        return Err("string-fill!: expected 2 arguments".to_string());
2953    }
2954
2955    let s = match arena.get(args[0]) {
2956        ValueData::String(s) => s.clone(),
2957        _ => return Err("string-fill!: first argument must be a string".to_string()),
2958    };
2959
2960    let c = match arena.get(args[1]) {
2961        ValueData::Char(c) => *c,
2962        _ => return Err("string-fill!: second argument must be a character".to_string()),
2963    };
2964
2965    let new_string: String = std::iter::repeat(c).take(s.len()).collect();
2966    Ok(arena.string(new_string))
2967}
2968
2969/// char-lower-case? - check if character is lowercase
2970pub fn char_lower_case_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2971    if args.len() != 1 {
2972        return Err("char-lower-case?: expected 1 argument".to_string());
2973    }
2974
2975    match arena.get(args[0]) {
2976        ValueData::Char(c) => Ok(if c.is_lowercase() { crate::scheme::arena::TRUE_ID } else { crate::scheme::arena::FALSE_ID }),
2977        _ => Err("char-lower-case?: argument must be a character".to_string()),
2978    }
2979}
2980
2981/// char-upper-case? - check if character is uppercase
2982pub fn char_upper_case_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2983    if args.len() != 1 {
2984        return Err("char-upper-case?: expected 1 argument".to_string());
2985    }
2986
2987    match arena.get(args[0]) {
2988        ValueData::Char(c) => Ok(if c.is_uppercase() { crate::scheme::arena::TRUE_ID } else { crate::scheme::arena::FALSE_ID }),
2989        _ => Err("char-upper-case?: argument must be a character".to_string()),
2990    }
2991}
2992
2993// =============================================================================
2994// Batch 15: List Utilities (5 primitives)
2995// =============================================================================
2996
2997/// last - return last element of list
2998pub fn last(arena: &Arena, args: &[ValueId]) -> ArenaResult {
2999    if args.len() != 1 {
3000        return Err("last: expected 1 argument".to_string());
3001    }
3002
3003    let mut current = args[0];
3004    let mut prev_car = crate::scheme::arena::NIL_ID;
3005
3006    loop {
3007        match arena.get(current) {
3008            ValueData::Nil => {
3009                if prev_car == crate::scheme::arena::NIL_ID {
3010                    return Err("last: argument must be a non-empty list".to_string());
3011                }
3012                return Ok(prev_car);
3013            }
3014            ValueData::Pair { car, cdr, .. } => {
3015                prev_car = *car;
3016                current = *cdr;
3017            }
3018            _ => return Err("last: argument must be a proper list".to_string()),
3019        }
3020    }
3021}
3022
3023/// last-pair - return last pair of list
3024pub fn last_pair(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3025    if args.len() != 1 {
3026        return Err("last-pair: expected 1 argument".to_string());
3027    }
3028
3029    let mut current = args[0];
3030
3031    // Check if empty list
3032    if matches!(arena.get(current), ValueData::Nil) {
3033        return Err("last-pair: argument must be a non-empty list".to_string());
3034    }
3035
3036    loop {
3037        match arena.get(current) {
3038            ValueData::Pair { cdr, .. } => {
3039                if matches!(arena.get(*cdr), ValueData::Nil) {
3040                    return Ok(current);
3041                }
3042                current = *cdr;
3043            }
3044            _ => return Err("last-pair: argument must be a proper list".to_string()),
3045        }
3046    }
3047}
3048
3049/// list-copy - shallow copy a list
3050pub fn list_copy(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3051    if args.len() != 1 {
3052        return Err("list-copy: expected 1 argument".to_string());
3053    }
3054
3055    let mut result = crate::scheme::arena::NIL_ID;
3056    let mut tail = crate::scheme::arena::NIL_ID;
3057    let mut current = args[0];
3058
3059    loop {
3060        // Extract car and cdr before mutating arena
3061        let (car_val, cdr_val) = match arena.get(current) {
3062            ValueData::Nil => {
3063                return Ok(result);
3064            }
3065            ValueData::Pair { car, cdr, .. } => (*car, *cdr),
3066            _ => return Err("list-copy: argument must be a proper list".to_string()),
3067        };
3068
3069        let new_pair = arena.cons(car_val, crate::scheme::arena::NIL_ID);
3070        if result == crate::scheme::arena::NIL_ID {
3071            result = new_pair;
3072            tail = new_pair;
3073        } else {
3074            // Update the cdr of the previous pair
3075            match arena.get_mut(tail) {
3076                ValueData::Pair { cdr: tail_cdr, .. } => {
3077                    *tail_cdr = new_pair;
3078                }
3079                _ => unreachable!(),
3080            }
3081            tail = new_pair;
3082        }
3083        current = cdr_val;
3084    }
3085}
3086
3087/// append! - destructive append (mutates last cdr of first list)
3088pub fn append_bang(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3089    if args.is_empty() {
3090        return Ok(crate::scheme::arena::NIL_ID);
3091    }
3092
3093    if args.len() == 1 {
3094        return Ok(args[0]);
3095    }
3096
3097    // Find the last pair of the first list
3098    let first = args[0];
3099    if matches!(arena.get(first), ValueData::Nil) {
3100        // If first list is empty, return the rest
3101        return append_bang(arena, &args[1..]);
3102    }
3103
3104    let mut current = first;
3105    loop {
3106        match arena.get(current) {
3107            ValueData::Pair { cdr, .. } => {
3108                if matches!(arena.get(*cdr), ValueData::Nil) {
3109                    // Found the last pair, mutate its cdr
3110                    let rest = if args.len() > 2 {
3111                        append_bang(arena, &args[1..])?
3112                    } else {
3113                        args[1]
3114                    };
3115                    match arena.get_mut(current) {
3116                        ValueData::Pair { cdr: last_cdr, .. } => {
3117                            *last_cdr = rest;
3118                        }
3119                        _ => unreachable!(),
3120                    }
3121                    return Ok(first);
3122                }
3123                current = *cdr;
3124            }
3125            _ => return Err("append!: arguments must be lists".to_string()),
3126        }
3127    }
3128}
3129
3130/// iota - generate list of integers from 0 to n-1
3131pub fn iota(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3132    if args.is_empty() || args.len() > 3 {
3133        return Err("iota: expected 1 to 3 arguments".to_string());
3134    }
3135
3136    let count = match arena.get(args[0]) {
3137        ValueData::Integer(n) => *n,
3138        _ => return Err("iota: first argument must be an integer".to_string()),
3139    };
3140
3141    let start = if args.len() > 1 {
3142        match arena.get(args[1]) {
3143            ValueData::Integer(n) => *n,
3144            _ => return Err("iota: second argument must be an integer".to_string()),
3145        }
3146    } else {
3147        0
3148    };
3149
3150    let step = if args.len() > 2 {
3151        match arena.get(args[2]) {
3152            ValueData::Integer(n) => *n,
3153            _ => return Err("iota: third argument must be an integer".to_string()),
3154        }
3155    } else {
3156        1
3157    };
3158
3159    if count < 0 {
3160        return Err("iota: count must be non-negative".to_string());
3161    }
3162
3163    let mut result = crate::scheme::arena::NIL_ID;
3164    for i in (0..count).rev() {
3165        let val = arena.int(start + i * step);
3166        result = arena.cons(val, result);
3167    }
3168
3169    Ok(result)
3170}
3171
3172// =============================================================================
3173// Batch 16: Extended List Operations (5 primitives)
3174// =============================================================================
3175
3176/// take - take first n elements of list
3177pub fn take(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3178    if args.len() != 2 {
3179        return Err("take: expected 2 arguments".to_string());
3180    }
3181
3182    let list = args[0];
3183    let n = match arena.get(args[1]) {
3184        ValueData::Integer(n) => *n,
3185        _ => return Err("take: second argument must be an integer".to_string()),
3186    };
3187
3188    if n < 0 {
3189        return Err("take: count must be non-negative".to_string());
3190    }
3191
3192    let mut result = crate::scheme::arena::NIL_ID;
3193    let mut tail = crate::scheme::arena::NIL_ID;
3194    let mut current = list;
3195    let mut count = 0;
3196
3197    while count < n {
3198        let (car_val, cdr_val) = match arena.get(current) {
3199            ValueData::Nil => {
3200                return Err(format!("take: list has only {} elements, cannot take {}", count, n));
3201            }
3202            ValueData::Pair { car, cdr, .. } => (*car, *cdr),
3203            _ => return Err("take: first argument must be a list".to_string()),
3204        };
3205
3206        let new_pair = arena.cons(car_val, crate::scheme::arena::NIL_ID);
3207        if result == crate::scheme::arena::NIL_ID {
3208            result = new_pair;
3209            tail = new_pair;
3210        } else {
3211            match arena.get_mut(tail) {
3212                ValueData::Pair { cdr: tail_cdr, .. } => {
3213                    *tail_cdr = new_pair;
3214                }
3215                _ => unreachable!(),
3216            }
3217            tail = new_pair;
3218        }
3219
3220        current = cdr_val;
3221        count += 1;
3222    }
3223
3224    Ok(result)
3225}
3226
3227/// drop - drop first n elements of list
3228pub fn drop(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3229    if args.len() != 2 {
3230        return Err("drop: expected 2 arguments".to_string());
3231    }
3232
3233    let mut list = args[0];
3234    let n = match arena.get(args[1]) {
3235        ValueData::Integer(n) => *n,
3236        _ => return Err("drop: second argument must be an integer".to_string()),
3237    };
3238
3239    if n < 0 {
3240        return Err("drop: count must be non-negative".to_string());
3241    }
3242
3243    for i in 0..n {
3244        match arena.get(list) {
3245            ValueData::Nil => {
3246                return Err(format!("drop: list has only {} elements, cannot drop {}", i, n));
3247            }
3248            ValueData::Pair { cdr, .. } => {
3249                list = *cdr;
3250            }
3251            _ => return Err("drop: first argument must be a list".to_string()),
3252        }
3253    }
3254
3255    Ok(list)
3256}
3257
3258/// split-at - split list at position n, returns (take n list) . (drop n list)
3259/// Note: Returns a pair of lists
3260pub fn split_at(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3261    if args.len() != 2 {
3262        return Err("split-at: expected 2 arguments".to_string());
3263    }
3264
3265    let taken = take(arena, args)?;
3266    let dropped = drop(arena, args)?;
3267
3268    Ok(arena.cons(taken, dropped))
3269}
3270
3271/// filter - filter list by predicate (requires evaluator context)
3272/// Note: This is a simplified version that can't call back to the evaluator
3273/// For now, we'll make it return an error suggesting use of the non-arena version
3274pub fn filter(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
3275    Err("filter: not available in arena mode (requires callback to evaluator)".to_string())
3276}
3277
3278/// remove - remove elements matching predicate (requires evaluator context)
3279/// Note: This is a simplified version that can't call back to the evaluator
3280/// For now, we'll make it return an error suggesting use of the non-arena version
3281pub fn remove(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
3282    Err("remove: not available in arena mode (requires callback to evaluator)".to_string())
3283}
3284
3285// =============================================================================
3286// Batch 17: Additional Numeric Operations (6 primitives)
3287// =============================================================================
3288
3289/// numerator - get numerator of rational (for now, just return the integer part)
3290pub fn numerator(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3291    if args.len() != 1 {
3292        return Err("numerator: expected 1 argument".to_string());
3293    }
3294
3295    match arena.get(args[0]) {
3296        ValueData::Integer(n) => Ok(arena.int(*n)),
3297        ValueData::Real(f) => {
3298            // For real numbers, treat as improper fraction with denominator 1
3299            Ok(arena.int(*f as i64))
3300        }
3301        _ => Err("numerator: argument must be a number".to_string()),
3302    }
3303}
3304
3305/// denominator - get denominator of rational (for now, always return 1)
3306pub fn denominator(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3307    if args.len() != 1 {
3308        return Err("denominator: expected 1 argument".to_string());
3309    }
3310
3311    match arena.get(args[0]) {
3312        ValueData::Integer(_) | ValueData::Real(_) => Ok(arena.int(1)),
3313        _ => Err("denominator: argument must be a number".to_string()),
3314    }
3315}
3316
3317/// rationalize - rationalize a number to within tolerance (simplified: just return the number)
3318pub fn rationalize(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3319    if args.len() != 2 {
3320        return Err("rationalize: expected 2 arguments".to_string());
3321    }
3322
3323    // Simplified: just return the first argument
3324    match arena.get(args[0]) {
3325        ValueData::Integer(_) | ValueData::Real(_) => Ok(args[0]),
3326        _ => Err("rationalize: first argument must be a number".to_string()),
3327    }
3328}
3329
3330/// angle - get angle of complex number (for real numbers, 0 or pi)
3331pub fn angle(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3332    if args.len() != 1 {
3333        return Err("angle: expected 1 argument".to_string());
3334    }
3335
3336    match arena.get(args[0]) {
3337        ValueData::Integer(n) => {
3338            if *n < 0 {
3339                Ok(arena.real(std::f64::consts::PI))
3340            } else {
3341                Ok(arena.real(0.0))
3342            }
3343        }
3344        ValueData::Real(f) => {
3345            if *f < 0.0 {
3346                Ok(arena.real(std::f64::consts::PI))
3347            } else {
3348                Ok(arena.real(0.0))
3349            }
3350        }
3351        _ => Err("angle: argument must be a number".to_string()),
3352    }
3353}
3354
3355/// magnitude - get magnitude of complex number (for real numbers, absolute value)
3356pub fn magnitude(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3357    if args.len() != 1 {
3358        return Err("magnitude: expected 1 argument".to_string());
3359    }
3360
3361    match arena.get(args[0]) {
3362        ValueData::Integer(n) => Ok(arena.int(n.abs())),
3363        ValueData::Real(f) => Ok(arena.real(f.abs())),
3364        _ => Err("magnitude: argument must be a number".to_string()),
3365    }
3366}
3367
3368/// string->number with optional radix (note: we already have arena_string_to_number)
3369/// This is an extended version that supports radix parameter
3370pub fn string_to_number_radix(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3371    if args.is_empty() || args.len() > 2 {
3372        return Err("string->number: expected 1 or 2 arguments".to_string());
3373    }
3374
3375    let s = match arena.get(args[0]) {
3376        ValueData::String(s) => s.clone(),
3377        _ => return Err("string->number: first argument must be a string".to_string()),
3378    };
3379
3380    let radix = if args.len() == 2 {
3381        match arena.get(args[1]) {
3382            ValueData::Integer(r) => *r as u32,
3383            _ => return Err("string->number: radix must be an integer".to_string()),
3384        }
3385    } else {
3386        10
3387    };
3388
3389    if radix < 2 || radix > 36 {
3390        return Err("string->number: radix must be between 2 and 36".to_string());
3391    }
3392
3393    // Try to parse as integer with given radix
3394    if let Ok(n) = i64::from_str_radix(&s, radix) {
3395        return Ok(arena.int(n));
3396    }
3397
3398    // For radix 10, also try floating point
3399    if radix == 10 {
3400        if let Ok(f) = s.parse::<f64>() {
3401            return Ok(arena.real(f));
3402        }
3403    }
3404
3405    // Return false on parse failure
3406    Ok(crate::scheme::arena::FALSE_ID)
3407}
3408
3409// =============================================================================
3410// Batch 18: Conversion and Utility Operations (5 primitives)
3411// =============================================================================
3412
3413/// number->string with radix support
3414pub fn number_to_string_radix(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3415    if args.is_empty() || args.len() > 2 {
3416        return Err("number->string: expected 1 or 2 arguments".to_string());
3417    }
3418
3419    let radix = if args.len() == 2 {
3420        match arena.get(args[1]) {
3421            ValueData::Integer(r) => *r as u32,
3422            _ => return Err("number->string: radix must be an integer".to_string()),
3423        }
3424    } else {
3425        10
3426    };
3427
3428    if radix < 2 || radix > 36 {
3429        return Err("number->string: radix must be between 2 and 36".to_string());
3430    }
3431
3432    match arena.get(args[0]) {
3433        ValueData::Integer(n) => {
3434            if radix == 10 {
3435                Ok(arena.string(n.to_string()))
3436            } else {
3437                // Convert to string in given radix
3438                let s = if *n < 0 {
3439                    format!("-{}", format_radix((-n) as u64, radix))
3440                } else {
3441                    format_radix(*n as u64, radix)
3442                };
3443                Ok(arena.string(s))
3444            }
3445        }
3446        ValueData::Real(f) => {
3447            if radix != 10 {
3448                return Err("number->string: non-decimal radix not supported for real numbers".to_string());
3449            }
3450            Ok(arena.string(f.to_string()))
3451        }
3452        _ => Err("number->string: first argument must be a number".to_string()),
3453    }
3454}
3455
3456/// Helper function to format number in given radix
3457fn format_radix(mut n: u64, radix: u32) -> String {
3458    if n == 0 {
3459        return "0".to_string();
3460    }
3461
3462    let digits = "0123456789abcdefghijklmnopqrstuvwxyz";
3463    let mut result = String::new();
3464
3465    while n > 0 {
3466        let digit = (n % radix as u64) as usize;
3467        result.insert(0, digits.chars().nth(digit).unwrap());
3468        n /= radix as u64;
3469    }
3470
3471    result
3472}
3473
3474/// list? - check if value is a proper list (improved version)
3475pub fn list_p_improved(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3476    if args.len() != 1 {
3477        return Err("list?: expected 1 argument".to_string());
3478    }
3479
3480    let mut current = args[0];
3481    let mut visited = std::collections::HashSet::new();
3482
3483    loop {
3484        // Detect cycles
3485        if !visited.insert(current) {
3486            // Cycle detected - not a proper list
3487            return Ok(crate::scheme::arena::FALSE_ID);
3488        }
3489
3490        match arena.get(current) {
3491            ValueData::Nil => return Ok(crate::scheme::arena::TRUE_ID),
3492            ValueData::Pair { cdr, .. } => {
3493                current = *cdr;
3494            }
3495            _ => return Ok(crate::scheme::arena::FALSE_ID),
3496        }
3497    }
3498}
3499
3500/// null-list? - check if value is empty list (alias for null?)
3501pub fn null_list_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3502    if args.len() != 1 {
3503        return Err("null-list?: expected 1 argument".to_string());
3504    }
3505
3506    match arena.get(args[0]) {
3507        ValueData::Nil => Ok(crate::scheme::arena::TRUE_ID),
3508        _ => Ok(crate::scheme::arena::FALSE_ID),
3509    }
3510}
3511
3512/// improper-list? - check if value is an improper list (ends with non-nil atom)
3513pub fn improper_list_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3514    if args.len() != 1 {
3515        return Err("improper-list?: expected 1 argument".to_string());
3516    }
3517
3518    let mut current = args[0];
3519
3520    loop {
3521        match arena.get(current) {
3522            ValueData::Nil => return Ok(crate::scheme::arena::FALSE_ID), // Proper list
3523            ValueData::Pair { cdr, .. } => {
3524                current = *cdr;
3525            }
3526            _ => return Ok(crate::scheme::arena::TRUE_ID), // Ends with non-nil atom
3527        }
3528    }
3529}
3530
3531/// circular-list? - check if list has a cycle
3532pub fn circular_list_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3533    if args.len() != 1 {
3534        return Err("circular-list?: expected 1 argument".to_string());
3535    }
3536
3537    // Use Floyd's cycle detection (tortoise and hare)
3538    let mut slow = args[0];
3539    let mut fast = args[0];
3540
3541    loop {
3542        // Move slow by 1
3543        match arena.get(slow) {
3544            ValueData::Pair { cdr, .. } => slow = *cdr,
3545            _ => return Ok(crate::scheme::arena::FALSE_ID), // Not a list or reached end
3546        }
3547
3548        // Move fast by 2
3549        match arena.get(fast) {
3550            ValueData::Pair { cdr: cdr1, .. } => {
3551                fast = *cdr1;
3552                match arena.get(fast) {
3553                    ValueData::Pair { cdr: cdr2, .. } => fast = *cdr2,
3554                    _ => return Ok(crate::scheme::arena::FALSE_ID), // Reached end
3555                }
3556            }
3557            _ => return Ok(crate::scheme::arena::FALSE_ID), // Not a list or reached end
3558        }
3559
3560        // Check if slow == fast (cycle detected)
3561        if slow == fast {
3562            return Ok(crate::scheme::arena::TRUE_ID);
3563        }
3564    }
3565}
3566
3567// =============================================================================
3568// Batch 19: Bitwise Operations (8 primitives)
3569// =============================================================================
3570
3571/// bitwise-and - bitwise AND of integers
3572pub fn bitwise_and(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3573    if args.is_empty() {
3574        return Ok(arena.int(-1)); // Identity element for AND
3575    }
3576
3577    let mut result = -1i64;
3578    for &arg in args {
3579        match arena.get(arg) {
3580            ValueData::Integer(n) => {
3581                result &= n;
3582            }
3583            _ => return Err("bitwise-and: all arguments must be integers".to_string()),
3584        }
3585    }
3586
3587    Ok(arena.int(result))
3588}
3589
3590/// bitwise-ior - bitwise inclusive OR of integers
3591pub fn bitwise_ior(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3592    if args.is_empty() {
3593        return Ok(arena.int(0)); // Identity element for OR
3594    }
3595
3596    let mut result = 0i64;
3597    for &arg in args {
3598        match arena.get(arg) {
3599            ValueData::Integer(n) => {
3600                result |= n;
3601            }
3602            _ => return Err("bitwise-ior: all arguments must be integers".to_string()),
3603        }
3604    }
3605
3606    Ok(arena.int(result))
3607}
3608
3609/// bitwise-xor - bitwise exclusive OR of integers
3610pub fn bitwise_xor(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3611    if args.is_empty() {
3612        return Ok(arena.int(0)); // Identity element for XOR
3613    }
3614
3615    let mut result = 0i64;
3616    for &arg in args {
3617        match arena.get(arg) {
3618            ValueData::Integer(n) => {
3619                result ^= n;
3620            }
3621            _ => return Err("bitwise-xor: all arguments must be integers".to_string()),
3622        }
3623    }
3624
3625    Ok(arena.int(result))
3626}
3627
3628/// bitwise-not - bitwise NOT (complement)
3629pub fn bitwise_not(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3630    if args.len() != 1 {
3631        return Err("bitwise-not: expected 1 argument".to_string());
3632    }
3633
3634    match arena.get(args[0]) {
3635        ValueData::Integer(n) => Ok(arena.int(!n)),
3636        _ => Err("bitwise-not: argument must be an integer".to_string()),
3637    }
3638}
3639
3640/// arithmetic-shift - arithmetic left/right shift
3641pub fn arithmetic_shift(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3642    if args.len() != 2 {
3643        return Err("arithmetic-shift: expected 2 arguments".to_string());
3644    }
3645
3646    let n = match arena.get(args[0]) {
3647        ValueData::Integer(n) => *n,
3648        _ => return Err("arithmetic-shift: first argument must be an integer".to_string()),
3649    };
3650
3651    let shift = match arena.get(args[1]) {
3652        ValueData::Integer(s) => *s,
3653        _ => return Err("arithmetic-shift: second argument must be an integer".to_string()),
3654    };
3655
3656    let result = if shift >= 0 {
3657        // Left shift
3658        if shift >= 64 {
3659            0 // Shift all bits out
3660        } else {
3661            n.wrapping_shl(shift as u32)
3662        }
3663    } else {
3664        // Right shift (arithmetic - preserves sign)
3665        if shift <= -64 {
3666            if n < 0 { -1 } else { 0 }
3667        } else {
3668            n >> (-shift)
3669        }
3670    };
3671
3672    Ok(arena.int(result))
3673}
3674
3675/// bit-extract - extract bits from position start to end
3676pub fn bit_extract(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3677    if args.len() != 3 {
3678        return Err("bit-extract: expected 3 arguments".to_string());
3679    }
3680
3681    let n = match arena.get(args[0]) {
3682        ValueData::Integer(n) => *n as u64,
3683        _ => return Err("bit-extract: first argument must be an integer".to_string()),
3684    };
3685
3686    let start = match arena.get(args[1]) {
3687        ValueData::Integer(s) => *s as u32,
3688        _ => return Err("bit-extract: second argument must be an integer".to_string()),
3689    };
3690
3691    let end = match arena.get(args[2]) {
3692        ValueData::Integer(e) => *e as u32,
3693        _ => return Err("bit-extract: third argument must be an integer".to_string()),
3694    };
3695
3696    if start > end {
3697        return Err("bit-extract: start must be <= end".to_string());
3698    }
3699
3700    if end > 64 {
3701        return Err("bit-extract: end must be <= 64".to_string());
3702    }
3703
3704    let count = end - start;
3705    let mask = if count >= 64 {
3706        u64::MAX
3707    } else {
3708        (1u64 << count) - 1
3709    };
3710
3711    let result = (n >> start) & mask;
3712    Ok(arena.int(result as i64))
3713}
3714
3715/// bitwise-bit-set? - test if bit at position is set
3716pub fn bitwise_bit_set_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3717    if args.len() != 2 {
3718        return Err("bitwise-bit-set?: expected 2 arguments".to_string());
3719    }
3720
3721    let n = match arena.get(args[0]) {
3722        ValueData::Integer(n) => *n,
3723        _ => return Err("bitwise-bit-set?: first argument must be an integer".to_string()),
3724    };
3725
3726    let pos = match arena.get(args[1]) {
3727        ValueData::Integer(p) => *p,
3728        _ => return Err("bitwise-bit-set?: second argument must be an integer".to_string()),
3729    };
3730
3731    if pos < 0 || pos >= 64 {
3732        return Err("bitwise-bit-set?: position must be between 0 and 63".to_string());
3733    }
3734
3735    let is_set = (n & (1i64 << pos)) != 0;
3736    Ok(if is_set { crate::scheme::arena::TRUE_ID } else { crate::scheme::arena::FALSE_ID })
3737}
3738
3739/// bitwise-bit-count - count number of set bits (population count)
3740pub fn bitwise_bit_count(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3741    if args.len() != 1 {
3742        return Err("bitwise-bit-count: expected 1 argument".to_string());
3743    }
3744
3745    match arena.get(args[0]) {
3746        ValueData::Integer(n) => {
3747            let count = if *n < 0 {
3748                // For negative numbers, count bits in two's complement representation
3749                (*n as u64).count_ones()
3750            } else {
3751                (*n as u64).count_ones()
3752            };
3753            Ok(arena.int(count as i64))
3754        }
3755        _ => Err("bitwise-bit-count: argument must be an integer".to_string()),
3756    }
3757}
3758
3759// =============================================================================
3760// Batch 20: I/O and Display Operations (6 primitives)
3761// =============================================================================
3762
3763/// display - display a value (note: arena version just converts to string)
3764pub fn display(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3765    if args.len() != 1 {
3766        return Err("display: expected 1 argument".to_string());
3767    }
3768
3769    // In arena mode, we can't actually print to stdout
3770    // Return unspecified to indicate success
3771    // The actual printing would happen in the non-arena version
3772    let _s = value_to_display_string(arena, args[0]);
3773    Ok(crate::scheme::arena::UNSPECIFIED_ID)
3774}
3775
3776/// Helper to convert value to display string
3777fn value_to_display_string(arena: &Arena, id: ValueId) -> String {
3778    match arena.get(id) {
3779        ValueData::Nil => "()".to_string(),
3780        ValueData::Bool(true) => "#t".to_string(),
3781        ValueData::Bool(false) => "#f".to_string(),
3782        ValueData::Integer(n) => n.to_string(),
3783        ValueData::Real(f) => f.to_string(),
3784        ValueData::Char(c) => c.to_string(),
3785        ValueData::String(s) => s.clone(),
3786        ValueData::Symbol(s) => s.to_string(),
3787        ValueData::Keyword(k) => format!("{}:", k),
3788        ValueData::Pair { .. } => {
3789            // Build list representation
3790            let mut result = String::from("(");
3791            let mut current = id;
3792            let mut first = true;
3793
3794            loop {
3795                match arena.get(current) {
3796                    ValueData::Pair { car, cdr, .. } => {
3797                        if !first {
3798                            result.push(' ');
3799                        }
3800                        first = false;
3801                        result.push_str(&value_to_display_string(arena, *car));
3802
3803                        current = *cdr;
3804                        if matches!(arena.get(current), ValueData::Nil) {
3805                            break;
3806                        } else if !matches!(arena.get(current), ValueData::Pair { .. }) {
3807                            result.push_str(" . ");
3808                            result.push_str(&value_to_display_string(arena, current));
3809                            break;
3810                        }
3811                    }
3812                    _ => break,
3813                }
3814            }
3815            result.push(')');
3816            result
3817        }
3818        ValueData::Vector(elements) => {
3819            let mut result = String::from("#(");
3820            for (i, elem) in elements.iter().enumerate() {
3821                if i > 0 {
3822                    result.push(' ');
3823                }
3824                result.push_str(&value_to_display_string(arena, *elem));
3825            }
3826            result.push(')');
3827            result
3828        }
3829        ValueData::Procedure(_) => "#<procedure>".to_string(),
3830        ValueData::Unspecified => "#<unspecified>".to_string(),
3831        ValueData::Error => "#<error>".to_string(),
3832        _ => "#<unknown>".to_string(),
3833    }
3834}
3835
3836/// newline - output a newline
3837pub fn newline(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
3838    if !args.is_empty() {
3839        return Err("newline: expected 0 arguments".to_string());
3840    }
3841
3842    // In arena mode, we can't actually print
3843    // Return unspecified
3844    Ok(crate::scheme::arena::UNSPECIFIED_ID)
3845}
3846
3847/// write - write a value in machine-readable format
3848pub fn write(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3849    if args.len() != 1 {
3850        return Err("write: expected 1 argument".to_string());
3851    }
3852
3853    // Similar to display but with quotes for strings
3854    let _s = value_to_write_string(arena, args[0]);
3855    Ok(crate::scheme::arena::UNSPECIFIED_ID)
3856}
3857
3858fn value_to_write_string(arena: &Arena, id: ValueId) -> String {
3859    match arena.get(id) {
3860        ValueData::String(s) => format!("\"{}\"", s.escape_default()),
3861        ValueData::Char(c) => format!("#\\{}", c),
3862        _ => value_to_display_string(arena, id),
3863    }
3864}
3865
3866/// write-char - write a character
3867pub fn write_char(arena: &Arena, args: &[ValueId]) -> ArenaResult {
3868    if args.len() != 1 {
3869        return Err("write-char: expected 1 argument".to_string());
3870    }
3871
3872    match arena.get(args[0]) {
3873        ValueData::Char(_) => {
3874            // In arena mode, we can't actually write
3875            Ok(crate::scheme::arena::UNSPECIFIED_ID)
3876        }
3877        _ => Err("write-char: argument must be a character".to_string()),
3878    }
3879}
3880
3881/// read-char - read a character (stub - not implementable in arena mode)
3882pub fn read_char(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
3883    if !args.is_empty() {
3884        return Err("read-char: expected 0 arguments".to_string());
3885    }
3886
3887    // Cannot implement in arena mode - requires I/O
3888    Err("read-char: not available in arena mode (requires I/O)".to_string())
3889}
3890
3891/// eof-object? - test if value is EOF object
3892pub fn eof_object_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
3893    if args.len() != 1 {
3894        return Err("eof-object?: expected 1 argument".to_string());
3895    }
3896
3897    // We don't have a specific EOF value in arena
3898    // Return false for now
3899    Ok(crate::scheme::arena::FALSE_ID)
3900}
3901
3902// =============================================================================
3903// Batch 21: Number Formatting (2 primitives)
3904// =============================================================================
3905
3906/// format-number - format a number according to format string
3907pub fn format_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
3908    if args.len() != 2 {
3909        return Err("format-number: expected 2 arguments".to_string());
3910    }
3911
3912    let n = match arena.get(args[0]) {
3913        ValueData::Integer(i) => *i,
3914        ValueData::Real(r) => *r as i64,
3915        _ => return Err("format-number: first argument must be a number".to_string()),
3916    };
3917
3918    let format = match arena.get(args[1]) {
3919        ValueData::String(s) => s.as_str(),
3920        ValueData::Symbol(s) => s.as_ref(),
3921        _ => return Err("format-number: second argument must be a string or symbol".to_string()),
3922    };
3923
3924    // Format implementation
3925    let result = match format {
3926        "1" | "decimal" => n.to_string(),
3927        "I" | "roman-upper" => {
3928            // Roman numerals (up to 20)
3929            let roman_str = match n {
3930                1 => "I",
3931                2 => "II",
3932                3 => "III",
3933                4 => "IV",
3934                5 => "V",
3935                6 => "VI",
3936                7 => "VII",
3937                8 => "VIII",
3938                9 => "IX",
3939                10 => "X",
3940                11 => "XI",
3941                12 => "XII",
3942                13 => "XIII",
3943                14 => "XIV",
3944                15 => "XV",
3945                16 => "XVI",
3946                17 => "XVII",
3947                18 => "XVIII",
3948                19 => "XIX",
3949                20 => "XX",
3950                _ => return Ok(arena.string(n.to_string())),
3951            };
3952            roman_str.to_string()
3953        }
3954        "i" | "roman-lower" => {
3955            let roman_str = match n {
3956                1 => "i",
3957                2 => "ii",
3958                3 => "iii",
3959                4 => "iv",
3960                5 => "v",
3961                6 => "vi",
3962                7 => "vii",
3963                8 => "viii",
3964                9 => "ix",
3965                10 => "x",
3966                11 => "xi",
3967                12 => "xii",
3968                13 => "xiii",
3969                14 => "xiv",
3970                15 => "xv",
3971                16 => "xvi",
3972                17 => "xvii",
3973                18 => "xviii",
3974                19 => "xix",
3975                20 => "xx",
3976                _ => return Ok(arena.string(n.to_string())),
3977            };
3978            roman_str.to_string()
3979        }
3980        "a" | "alpha-lower" => {
3981            // Lowercase letters (1=a, 2=b, etc.)
3982            if n >= 1 && n <= 26 {
3983                ((b'a' + (n as u8 - 1)) as char).to_string()
3984            } else {
3985                n.to_string()
3986            }
3987        }
3988        "A" | "alpha-upper" => {
3989            // Uppercase letters (1=A, 2=B, etc.)
3990            if n >= 1 && n <= 26 {
3991                ((b'A' + (n as u8 - 1)) as char).to_string()
3992            } else {
3993                n.to_string()
3994            }
3995        }
3996        _ => n.to_string(), // Default to decimal
3997    };
3998
3999    Ok(arena.string(result))
4000}
4001
4002/// format-number-list - join a list of numbers with separator
4003pub fn format_number_list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4004    if args.is_empty() || args.len() > 2 {
4005        return Err("format-number-list: expected 1 or 2 arguments".to_string());
4006    }
4007
4008    let separator = if args.len() == 2 {
4009        match arena.get(args[1]) {
4010            ValueData::String(s) => s.as_str(),
4011            ValueData::Symbol(s) => s.as_ref(),
4012            _ => ".",
4013        }
4014    } else {
4015        "."
4016    };
4017
4018    let mut numbers = Vec::new();
4019    let mut current = args[0];
4020
4021    loop {
4022        match arena.get(current) {
4023            ValueData::Nil => break,
4024            ValueData::Pair { car, cdr, .. } => {
4025                match arena.get(*car) {
4026                    ValueData::Integer(n) => numbers.push(n.to_string()),
4027                    ValueData::Real(r) => numbers.push((*r as i64).to_string()),
4028                    _ => {
4029                        return Err(
4030                            "format-number-list: list must contain only numbers".to_string()
4031                        )
4032                    }
4033                }
4034                current = *cdr;
4035            }
4036            _ => return Err("format-number-list: not a proper list".to_string()),
4037        }
4038    }
4039
4040    Ok(arena.string(numbers.join(separator)))
4041}
4042
4043// =============================================================================
4044// Batch 22: Simple Constants (1 primitive)
4045// =============================================================================
4046
4047/// empty-sosofo - return an empty sosofo (flow object)
4048pub fn empty_sosofo(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4049    if !args.is_empty() {
4050        return Err("empty-sosofo: expected no arguments".to_string());
4051    }
4052
4053    Ok(arena.alloc(ValueData::Sosofo))
4054}
4055
4056/// (sosofo-append sosofo ...) → sosofo
4057/// Returns a sosofo that appends multiple sosofos.
4058/// OpenJade: returns empty-sosofo when called with 0 arguments.
4059pub fn sosofo_append(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4060    // OpenJade: returns empty-sosofo when called with 0 arguments
4061    if args.is_empty() {
4062        return Ok(arena.alloc(ValueData::Sosofo));
4063    }
4064
4065    // Check all arguments are sosofos, unspecified, or nil (which we treat as empty sosofo)
4066    for &arg in args {
4067        match arena.get(arg) {
4068            ValueData::Sosofo | ValueData::Unspecified | ValueData::Nil => {},
4069            _ => return Err(format!("sosofo-append: not a sosofo")),
4070        }
4071    }
4072
4073    // Return a sosofo (placeholder for now)
4074    // In practice, sosofos are just markers - the actual output happens via backend
4075    Ok(arena.alloc(ValueData::Sosofo))
4076}
4077
4078/// (if-first-page then-sosofo else-sosofo) → sosofo
4079/// Returns then-sosofo if on the first page, else-sosofo otherwise.
4080/// This is a stub implementation for RTF backend - always returns else-sosofo (not first page).
4081#[inline]
4082pub fn if_first_page(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4083    if args.len() != 2 {
4084        return Err("if-first-page: expected 2 arguments (then-sosofo else-sosofo)".to_string());
4085    }
4086
4087    // Stub: always return else-sosofo (second argument)
4088    // In a full RTF implementation, this would check page state
4089    Ok(args[1])
4090}
4091
4092/// (if-front-page then-sosofo else-sosofo) → sosofo
4093/// Returns then-sosofo if in front matter, else-sosofo otherwise.
4094/// This is a stub implementation for RTF backend - always returns else-sosofo (not in front matter).
4095#[inline]
4096pub fn if_front_page(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4097    if args.len() != 2 {
4098        return Err("if-front-page: expected 2 arguments (then-sosofo else-sosofo)".to_string());
4099    }
4100
4101    // Stub: always return else-sosofo (second argument)
4102    // In a full RTF implementation, this would check if we're in front matter
4103    Ok(args[1])
4104}
4105
4106// =============================================================================
4107// Batch 23: Grove Operations - Current Node (1 primitive)
4108// =============================================================================
4109
4110/// current-node - return the current node in the grove
4111pub fn current_node(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4112    if !args.is_empty() {
4113        return Err("current-node: expected no arguments".to_string());
4114    }
4115
4116    // Access grove state directly from arena (unified pattern!)
4117    match &arena.current_node {
4118        Some(node) => Ok(arena.alloc(ValueData::Node(node.clone()))),
4119        None => Err("current-node: no current node set".to_string()),
4120    }
4121}
4122
4123// =============================================================================
4124// Batch 24: Grove Operations - Node Properties (3 primitives)
4125// =============================================================================
4126
4127/// gi - return the element name (generic identifier) of a node
4128pub fn gi(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4129    let node = if args.is_empty() {
4130        // No argument: use current node
4131        match &arena.current_node {
4132            Some(n) => n.clone(),
4133            None => return Err("gi: no current node".to_string()),
4134        }
4135    } else if args.len() == 1 {
4136        // OpenJade: optSingletonNodeList pattern - accept node, singleton node-list, #f, or symbol
4137        match arena.get(args[0]) {
4138            ValueData::Bool(false) => {
4139                // #f means no node - return #f
4140                return Ok(crate::scheme::arena::FALSE_ID);
4141            }
4142            ValueData::Symbol(_s) => {
4143                // Symbol passed directly - return it unchanged (OpenJade compatibility)
4144                return Ok(args[0]);
4145            }
4146            ValueData::Node(n) => n.clone(),
4147            ValueData::NodeList(nl) => {
4148                if nl.is_empty() {
4149                    // Empty node-list -> return false
4150                    return Ok(crate::scheme::arena::FALSE_ID);
4151                }
4152                let first = nl.first();
4153                let rest = nl.rest();
4154                if rest.is_empty() {
4155                    // Singleton node-list -> extract the single node
4156                    Rc::new(first.unwrap())
4157                } else {
4158                    // Multi-element node-list -> error (not a singleton)
4159                    return Err("gi: node-list must be a singleton".to_string());
4160                }
4161            }
4162            other => {
4163                let type_name = match other {
4164                    ValueData::Nil => "nil",
4165                    ValueData::Bool(_) => "boolean",
4166                    ValueData::Integer(_) => "integer",
4167                    ValueData::Real(_) => "real",
4168                    ValueData::Quantity { .. } => "quantity",
4169                    ValueData::Char(_) => "char",
4170                    ValueData::String(_) => "string",
4171                    ValueData::Keyword(_) => "keyword",
4172                    ValueData::Pair { .. } => "pair",
4173                    ValueData::Vector(_) => "vector",
4174                    ValueData::Procedure(_) => "procedure",
4175                    ValueData::Sosofo => "sosofo",
4176                    ValueData::Unspecified => "unspecified",
4177                    _ => "unknown",
4178                };
4179                return Err(format!("gi: argument must be a node, node-list, #f, or symbol (got {})", type_name));
4180            }
4181        }
4182    } else {
4183        return Err("gi: expected 0 or 1 arguments".to_string());
4184    };
4185
4186    // Get element name
4187    match node.gi() {
4188        Some(name) => Ok(arena.symbol(name.into())),
4189        None => Ok(crate::scheme::arena::FALSE_ID),
4190    }
4191}
4192
4193/// data - return the text content of a node
4194pub fn data(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4195    // OpenJade: data accepts a node-list and concatenates data from all nodes
4196    if args.len() > 1 {
4197        return Err("data: expected 0 or 1 arguments".to_string());
4198    }
4199
4200    let node_list = if args.is_empty() {
4201        // No argument: use current node as singleton node-list
4202        match &arena.current_node {
4203            Some(n) => {
4204                let singleton = crate::grove::VecNodeList::new(vec![n.clone_node()]);
4205                Box::new(singleton) as Box<dyn crate::grove::NodeList>
4206            }
4207            None => return Err("data: no current node".to_string()),
4208        }
4209    } else {
4210        // One argument: accept node or node-list
4211        match arena.get(args[0]) {
4212            ValueData::Node(n) => {
4213                // Wrap node in singleton node-list
4214                let singleton = crate::grove::VecNodeList::new(vec![n.clone_node()]);
4215                Box::new(singleton) as Box<dyn crate::grove::NodeList>
4216            }
4217            ValueData::NodeList(nl) => {
4218                // Clone the node-list (nl is Rc<Box<dyn NodeList>>)
4219                let vec_nodes: Vec<Box<dyn crate::grove::Node>> = {
4220                    let mut result = Vec::new();
4221                    let mut current = nl.clone();
4222                    loop {
4223                        if current.is_empty() {
4224                            break;
4225                        }
4226                        result.push(current.first().unwrap());
4227                        current = Rc::new(current.rest());
4228                    }
4229                    result
4230                };
4231                Box::new(crate::grove::VecNodeList::new(vec_nodes)) as Box<dyn crate::grove::NodeList>
4232            }
4233            _ => return Err("data: argument must be a node or node-list".to_string()),
4234        }
4235    };
4236
4237    // Concatenate data from all nodes in the node-list
4238    // OpenJade: returns empty string for empty node-list, not #f
4239    let mut result = String::new();
4240    let mut current_list: Box<dyn crate::grove::NodeList> = node_list;
4241    loop {
4242        if current_list.is_empty() {
4243            break;
4244        }
4245        let node = current_list.first().unwrap();
4246        if let Some(text) = node.data() {
4247            result.push_str(&text);
4248        }
4249        current_list = current_list.rest();
4250    }
4251
4252    // Always return a string (possibly empty), never #f
4253    Ok(arena.string(result))
4254}
4255
4256/// id - return the ID attribute of a node
4257pub fn id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4258    let node = if args.is_empty() {
4259        // No argument: use current node
4260        match &arena.current_node {
4261            Some(n) => n.clone(),
4262            None => return Err("id: no current node".to_string()),
4263        }
4264    } else if args.len() == 1 {
4265        // One argument: extract node from ValueId (optSingletonNodeList pattern)
4266        match arena.get(args[0]) {
4267            ValueData::Node(n) => n.clone(),
4268            ValueData::NodeList(nl) => {
4269                if nl.is_empty() {
4270                    // Empty node-list -> return #f
4271                    return Ok(crate::scheme::arena::FALSE_ID);
4272                }
4273                let first = nl.first();
4274                let rest = nl.rest();
4275                if rest.is_empty() {
4276                    // Singleton node-list -> extract the node
4277                    Rc::new(first.unwrap())
4278                } else {
4279                    // Multi-element node-list -> error
4280                    return Err("id: node-list must be a singleton".to_string());
4281                }
4282            }
4283            // OpenJade: NULL nodes return #f for id lookups
4284            _ => return Ok(crate::scheme::arena::FALSE_ID),
4285        }
4286    } else {
4287        return Err("id: expected 0 or 1 arguments".to_string());
4288    };
4289
4290    // Get ID attribute
4291    match node.id() {
4292        Some(id_str) => Ok(arena.string(id_str)),
4293        None => Ok(crate::scheme::arena::FALSE_ID),
4294    }
4295}
4296
4297// =============================================================================
4298// Batch 25: Grove Operations - Navigation (3 primitives)
4299// =============================================================================
4300
4301/// children - return the children of a node as a node-list
4302pub fn children(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4303    let node = if args.is_empty() {
4304        // No argument: use current node
4305        match &arena.current_node {
4306            Some(n) => n.clone(),
4307            None => return Err("children: no current node".to_string()),
4308        }
4309    } else if args.len() == 1 {
4310        // One argument: extract node from ValueId
4311        // OpenJade: optSingletonNodeList pattern - accept node or singleton node-list
4312        match arena.get(args[0]) {
4313            ValueData::Node(n) => n.clone(),
4314            ValueData::NodeList(nl) => {
4315                // If it's a node-list, try to extract singleton
4316                if nl.is_empty() {
4317                    // Empty node-list -> return empty node-list
4318                    return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::VecNodeList::new(vec![]))))));
4319                }
4320                let first = nl.first();
4321                let rest = nl.rest();
4322                if rest.is_empty() {
4323                    // Singleton node-list -> extract the single node
4324                    Rc::new(first.unwrap())
4325                } else {
4326                    // Multi-element node-list -> map children over all nodes
4327                    let mut all_children = Vec::new();
4328                    let mut current = nl.as_ref();
4329                    while !current.is_empty() {
4330                        if let Some(node) = current.first() {
4331                            let node_children = node.children();
4332                            let mut child_current = Box::new(node_children);
4333                            while !child_current.is_empty() {
4334                                if let Some(child) = child_current.first() {
4335                                    all_children.push(child);
4336                                }
4337                                child_current = Box::new(child_current.rest());
4338                            }
4339                        }
4340                        current = Box::leak(Box::new(current.rest()));
4341                    }
4342                    let result = crate::grove::VecNodeList::new(all_children);
4343                    return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(result)))));
4344                }
4345            }
4346            _ => return Err("children: argument must be a node or node-list".to_string()),
4347        }
4348    } else {
4349        return Err("children: expected 0 or 1 arguments".to_string());
4350    };
4351
4352    // Get children
4353    let children = node.children();
4354    Ok(arena.alloc(ValueData::NodeList(Rc::new(children))))
4355}
4356
4357/// parent - return the parent of a node
4358pub fn parent(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4359    let node = if args.is_empty() {
4360        // No argument: use current node
4361        match &arena.current_node {
4362            Some(n) => n.clone(),
4363            None => return Err("parent: no current node".to_string()),
4364        }
4365    } else if args.len() == 1 {
4366        // OpenJade: optSingletonNodeList pattern - accept node or singleton node-list
4367        match arena.get(args[0]) {
4368            ValueData::Node(n) => n.clone(),
4369            ValueData::NodeList(nl) => {
4370                if nl.is_empty() {
4371                    // Empty node-list -> return empty node-list (not FALSE!)
4372                    return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))));
4373                }
4374                let first = nl.first();
4375                let rest = nl.rest();
4376                if rest.is_empty() {
4377                    // Singleton node-list -> extract the node
4378                    Rc::new(first.unwrap())
4379                } else {
4380                    // Multi-element node-list -> error
4381                    return Err("parent: node-list must be a singleton".to_string());
4382                }
4383            }
4384            _ => return Err("parent: argument must be a node or node-list".to_string()),
4385        }
4386    } else {
4387        return Err("parent: expected 0 or 1 arguments".to_string());
4388    };
4389
4390    // Get parent - OpenJade returns singleton node-list or empty node-list
4391    match node.parent() {
4392        Some(parent) => {
4393            use crate::grove::VecNodeList;
4394            let nodes = vec![parent.clone_node()];
4395            Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(VecNodeList::new(nodes))))))
4396        }
4397        None => Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList))))),
4398    }
4399}
4400
4401/// attributes - return the attributes of a node as a node-list
4402pub fn attributes(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4403    let _node = if args.is_empty() {
4404        // No argument: use current node
4405        match &arena.current_node {
4406            Some(n) => n.clone(),
4407            None => return Err("attributes: no current node".to_string()),
4408        }
4409    } else if args.len() == 1 {
4410        // One argument: extract node from ValueId
4411        match arena.get(args[0]) {
4412            ValueData::Node(n) => n.clone(),
4413            _ => return Err("attributes: argument must be a node".to_string()),
4414        }
4415    } else {
4416        return Err("attributes: expected 0 or 1 arguments".to_string());
4417    };
4418
4419    // TODO: Implement attribute node-list
4420    // For now, return empty node-list (stub - same as existing primitives.rs implementation)
4421    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList::new())))))
4422}
4423
4424// =============================================================================
4425// Batch 26: Node-list Operations (5 primitives)
4426// =============================================================================
4427
4428/// node-list? - check if value is a node-list
4429pub fn node_list_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
4430    if args.len() != 1 {
4431        return Err("node-list?: expected 1 argument".to_string());
4432    }
4433
4434    match arena.get(args[0]) {
4435        ValueData::NodeList(_) => Ok(crate::scheme::arena::TRUE_ID),
4436        _ => Ok(crate::scheme::arena::FALSE_ID),
4437    }
4438}
4439
4440/// empty-node-list - return an empty node-list
4441pub fn empty_node_list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4442    if !args.is_empty() {
4443        return Err("empty-node-list: expected no arguments".to_string());
4444    }
4445
4446    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList::new())))))
4447}
4448
4449/// node-list-empty? - check if node-list is empty
4450pub fn node_list_empty_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
4451    if args.len() != 1 {
4452        return Err("node-list-empty?: expected 1 argument".to_string());
4453    }
4454
4455    // OpenJade: optSingletonNodeList pattern - accept node or node-list
4456    match arena.get(args[0]) {
4457        ValueData::NodeList(nl) => {
4458            if nl.is_empty() {
4459                Ok(crate::scheme::arena::TRUE_ID)
4460            } else {
4461                Ok(crate::scheme::arena::FALSE_ID)
4462            }
4463        }
4464        ValueData::Node(_) => {
4465            // Single node -> non-empty node-list
4466            Ok(crate::scheme::arena::FALSE_ID)
4467        }
4468        _ => Err("node-list-empty?: argument must be a node or node-list".to_string()),
4469    }
4470}
4471
4472/// node-list-length - return the length of a node-list
4473pub fn node_list_length(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4474    if args.len() != 1 {
4475        return Err("node-list-length: expected 1 argument".to_string());
4476    }
4477
4478    match arena.get(args[0]) {
4479        ValueData::NodeList(nl) => Ok(arena.int(nl.length() as i64)),
4480        _ => Err("node-list-length: argument must be a node-list".to_string()),
4481    }
4482}
4483
4484/// node-list-first - return the first node in a node-list
4485pub fn node_list_first(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4486    if args.len() != 1 {
4487        return Err("node-list-first: expected 1 argument".to_string());
4488    }
4489
4490    match arena.get(args[0]) {
4491        ValueData::NodeList(nl) => {
4492            match nl.first() {
4493                Some(node) => Ok(arena.alloc(ValueData::Node(Rc::new(node)))),
4494                // OpenJade returns a singleton containing a null node
4495                // We can't represent null nodes safely, so return #f instead
4496                None => Ok(crate::scheme::arena::FALSE_ID),
4497            }
4498        }
4499        _ => Err("node-list-first: argument must be a node-list".to_string()),
4500    }
4501}
4502
4503// =============================================================================
4504// Batch 27: Attribute Operations (1 primitive)
4505// =============================================================================
4506
4507/// attribute-string - get the value of a named attribute
4508/// (attribute-string attr-name [node]) → string or #f
4509pub fn attribute_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4510    if args.is_empty() || args.len() > 2 {
4511        return Err("attribute-string: expected 1 or 2 arguments".to_string());
4512    }
4513
4514    // First argument: attribute name (must be string or symbol)
4515    let attr_name = match arena.get(args[0]) {
4516        ValueData::String(s) => s.clone(),
4517        ValueData::Symbol(s) => s.to_string(),
4518        _ => return Err("attribute-string: first argument must be a string or symbol".to_string()),
4519    };
4520
4521    // Second argument (optional): node or node-list (OpenJade: optSingletonNodeList pattern)
4522    let node = if args.len() == 2 {
4523        match arena.get(args[1]) {
4524            ValueData::Node(n) => Some(n.clone()),
4525            ValueData::NodeList(nl) => {
4526                if nl.is_empty() {
4527                    // Empty node-list -> return #f
4528                    return Ok(crate::scheme::arena::FALSE_ID);
4529                }
4530                let first = nl.first();
4531                let rest = nl.rest();
4532                if rest.is_empty() {
4533                    // Singleton node-list -> extract the node
4534                    Some(Rc::new(first.unwrap()))
4535                } else {
4536                    // Multi-element node-list -> error
4537                    return Err("attribute-string: node-list must be a singleton".to_string());
4538                }
4539            }
4540            // OpenJade: NULL nodes return #f for attribute lookups
4541            // In Dazzle, node-list-first on empty returns #f, so accept it here
4542            _ => return Ok(crate::scheme::arena::FALSE_ID),
4543        }
4544    } else {
4545        // Use current node
4546        arena.current_node.clone()
4547    };
4548
4549    // If no node, return #f
4550    let node = match node {
4551        Some(n) => n,
4552        None => return Err("attribute-string: no current node".to_string()),
4553    };
4554
4555    // Get attribute value
4556    match node.attribute_string(&attr_name) {
4557        Some(value) => Ok(arena.string(value)),
4558        None => Ok(crate::scheme::arena::FALSE_ID),
4559    }
4560}
4561
4562// =============================================================================
4563// Batch 28: More Node-list Operations (3 primitives)
4564// =============================================================================
4565
4566/// node-list-rest - return all but the first node in a node-list
4567pub fn node_list_rest(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4568    if args.len() != 1 {
4569        return Err("node-list-rest: expected 1 argument".to_string());
4570    }
4571
4572    match arena.get(args[0]) {
4573        ValueData::NodeList(nl) => {
4574            let rest = nl.rest();
4575            Ok(arena.alloc(ValueData::NodeList(Rc::new(rest))))
4576        }
4577        _ => Err("node-list-rest: argument must be a node-list".to_string()),
4578    }
4579}
4580
4581/// node-list-ref - return the nth node in a node-list (0-indexed)
4582pub fn node_list_ref(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4583    if args.len() != 2 {
4584        return Err("node-list-ref: expected 2 arguments".to_string());
4585    }
4586
4587    let nl = match arena.get(args[0]) {
4588        ValueData::NodeList(nl) => nl.clone(),
4589        _ => return Err("node-list-ref: first argument must be a node-list".to_string()),
4590    };
4591
4592    let index = match arena.get(args[1]) {
4593        ValueData::Integer(n) => {
4594            if *n < 0 {
4595                return Err("node-list-ref: index must be non-negative".to_string());
4596            }
4597            *n as usize
4598        }
4599        _ => return Err("node-list-ref: second argument must be an integer".to_string()),
4600    };
4601
4602    // Iterate to the nth element
4603    let mut current = nl.as_ref();
4604    let mut i = 0;
4605    while i < index {
4606        if current.is_empty() {
4607            // DSSSL: out of bounds returns #f (not an error)
4608            return Ok(crate::scheme::arena::FALSE_ID);
4609        }
4610        let rest = current.rest();
4611        current = Box::leak(Box::new(rest)); // Safe because we're just reading
4612        i += 1;
4613    }
4614
4615    match current.first() {
4616        Some(node) => Ok(arena.alloc(ValueData::Node(Rc::new(node)))),
4617        // DSSSL: out of bounds returns #f (not an error)
4618        None => Ok(crate::scheme::arena::FALSE_ID),
4619    }
4620}
4621
4622/// node-list-reverse - reverse a node-list
4623pub fn node_list_reverse(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4624    if args.len() != 1 {
4625        return Err("node-list-reverse: expected 1 argument".to_string());
4626    }
4627
4628    match arena.get(args[0]) {
4629        ValueData::NodeList(nl) => {
4630            // Collect all nodes into a vector
4631            let mut nodes = Vec::new();
4632            let mut current = nl.as_ref();
4633            while !current.is_empty() {
4634                if let Some(node) = current.first() {
4635                    nodes.push(node);
4636                }
4637                current = Box::leak(Box::new(current.rest()));
4638            }
4639
4640            // Reverse the vector
4641            nodes.reverse();
4642
4643            // Create a reversed node-list by building from the back
4644            let reversed = crate::grove::VecNodeList::new(nodes);
4645            Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(reversed)))))
4646        }
4647        _ => Err("node-list-reverse: argument must be a node-list".to_string()),
4648    }
4649}
4650
4651// =============================================================================
4652// Batch 29: Type Predicates (3 primitives)
4653// =============================================================================
4654
4655/// node? - check if value is a node
4656pub fn node_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
4657    if args.len() != 1 {
4658        return Err("node?: expected 1 argument".to_string());
4659    }
4660
4661    match arena.get(args[0]) {
4662        ValueData::Node(_) => Ok(crate::scheme::arena::TRUE_ID),
4663        _ => Ok(crate::scheme::arena::FALSE_ID),
4664    }
4665}
4666
4667/// sosofo? - check if value is a sosofo
4668pub fn sosofo_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
4669    if args.len() != 1 {
4670        return Err("sosofo?: expected 1 argument".to_string());
4671    }
4672
4673    match arena.get(args[0]) {
4674        ValueData::Sosofo => Ok(crate::scheme::arena::TRUE_ID),
4675        _ => Ok(crate::scheme::arena::FALSE_ID),
4676    }
4677}
4678
4679/// quantity? - check if value is a quantity (stub - returns #f)
4680pub fn quantity_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
4681    if args.len() != 1 {
4682        return Err("quantity?: expected 1 argument".to_string());
4683    }
4684
4685    match arena.get(args[0]) {
4686        ValueData::Quantity { .. } => Ok(crate::scheme::arena::TRUE_ID),
4687        _ => Ok(crate::scheme::arena::FALSE_ID),
4688    }
4689}
4690
4691// =============================================================================
4692// Batch 30: Color and Spacing Stubs (4 primitives)
4693// =============================================================================
4694
4695/// color? - check if value is a color (stub - always returns #f)
4696pub fn color_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
4697    if args.len() != 1 {
4698        return Err("color?: expected 1 argument".to_string());
4699    }
4700    // No color type in our implementation yet - always false
4701    Ok(crate::scheme::arena::FALSE_ID)
4702}
4703
4704/// color - create a color (stub - returns #f)
4705pub fn color(_arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
4706    // Stub: color creation not implemented
4707    // TODO: Implement if needed for document formatting
4708    Ok(crate::scheme::arena::FALSE_ID)
4709}
4710
4711/// display-space? - check if value is display-space (stub - always returns #f)
4712pub fn display_space_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
4713    if args.len() != 1 {
4714        return Err("display-space?: expected 1 argument".to_string());
4715    }
4716    // No display-space type in our implementation yet - always false
4717    Ok(crate::scheme::arena::FALSE_ID)
4718}
4719
4720/// inline-space? - check if value is inline-space (stub - always returns #f)
4721pub fn inline_space_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
4722    if args.len() != 1 {
4723        return Err("inline-space?: expected 1 argument".to_string());
4724    }
4725    // No inline-space type in our implementation yet - always false
4726    Ok(crate::scheme::arena::FALSE_ID)
4727}
4728
4729// =============================================================================
4730// Batch 31: Quantity Operations (5 primitives - stubs)
4731// =============================================================================
4732
4733/// quantity->number - convert quantity to number (stub - returns 0)
4734pub fn quantity_to_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4735    if args.len() != 1 {
4736        return Err("quantity->number: expected 1 argument".to_string());
4737    }
4738
4739    match arena.get(args[0]) {
4740        ValueData::Quantity { magnitude, .. } => Ok(arena.real(*magnitude)),
4741        _ => Err("quantity->number: argument must be a quantity".to_string()),
4742    }
4743}
4744
4745/// number->quantity - convert number to quantity (stub - returns number as-is)
4746pub fn number_to_quantity(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4747    if args.len() < 2 || args.len() > 3 {
4748        return Err("number->quantity: expected 2 or 3 arguments".to_string());
4749    }
4750
4751    // For now, just return the number (stub)
4752    // TODO: Implement proper quantity types if needed for document formatting
4753    Ok(args[0])
4754}
4755
4756/// quantity-convert - convert quantity between units (stub - returns input)
4757pub fn quantity_convert(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4758    if args.len() != 2 {
4759        return Err("quantity-convert: expected 2 arguments".to_string());
4760    }
4761
4762    // Stub: just return the input quantity
4763    Ok(args[0])
4764}
4765
4766/// device-length - convert length to device units (stub - returns 0)
4767pub fn device_length(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4768    if args.len() != 1 {
4769        return Err("device-length: expected 1 argument".to_string());
4770    }
4771
4772    // Stub: return 0
4773    Ok(arena.int(0))
4774}
4775
4776/// label-distance - get label distance (stub - returns 0)
4777pub fn label_distance(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
4778    // Stub: return 0
4779    Ok(arena.int(0))
4780}
4781
4782// ============================================================================
4783// Phase 3 Batch 32: Grove navigation operations (5)
4784// ============================================================================
4785
4786/// ancestor - get ancestor with specified generic identifier
4787pub fn ancestor(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4788    if args.is_empty() || args.len() > 2 {
4789        return Err("ancestor: expected 1 or 2 arguments".to_string());
4790    }
4791
4792    // Get the ancestor name (first arg)
4793    let ancestor_name = match arena.get(args[0]) {
4794        ValueData::String(s) => s.clone(),
4795        ValueData::Symbol(s) => s.to_string(),
4796        _ => return Err("ancestor: first argument must be a string or symbol".to_string()),
4797    };
4798
4799    // Get the starting node (second arg or current node)
4800    // OpenJade: optSingletonNodeList pattern - accept node or singleton node-list
4801    let node = if args.len() == 2 {
4802        match arena.get(args[1]) {
4803            ValueData::Node(n) => n.clone(),
4804            ValueData::NodeList(nl) => {
4805                if nl.is_empty() {
4806                    // Empty node-list -> return empty node-list
4807                    return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))));
4808                }
4809                let first = nl.first();
4810                let rest = nl.rest();
4811                if rest.is_empty() {
4812                    // Singleton node-list -> extract the node
4813                    Rc::new(first.unwrap())
4814                } else {
4815                    // Multi-element node-list -> error
4816                    return Err("ancestor: node-list must be a singleton".to_string());
4817                }
4818            }
4819            _ => return Err("ancestor: second argument must be a node or singleton node-list".to_string()),
4820        }
4821    } else {
4822        match &arena.current_node {
4823            Some(n) => n.clone(),
4824            None => return Err("ancestor: no current node".to_string()),
4825        }
4826    };
4827
4828    // Walk up the tree looking for matching ancestor
4829    let mut current = node.parent();
4830    while let Some(parent_node) = current {
4831        if let Some(gi) = parent_node.gi() {
4832            if gi == ancestor_name {
4833                // Found - return singleton node-list (OpenJade semantics)
4834                use crate::grove::VecNodeList;
4835                let nodes = vec![parent_node.clone_node()];
4836                return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(VecNodeList::new(nodes))))));
4837            }
4838        }
4839        current = parent_node.parent();
4840    }
4841
4842    // Not found - return empty node-list (OpenJade semantics)
4843    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))))
4844}
4845
4846/// descendants - get all descendants of a node or node-list
4847pub fn descendants(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4848    if args.len() > 1 {
4849        return Err("descendants: expected 0 or 1 arguments".to_string());
4850    }
4851
4852    // Helper: Collect all descendants recursively
4853    fn collect_descendants(node: &dyn crate::grove::Node, result: &mut Vec<Box<dyn crate::grove::Node>>) {
4854        let children = node.children();
4855        let mut current = children.as_ref();
4856        while !current.is_empty() {
4857            if let Some(child) = current.first() {
4858                collect_descendants(child.as_ref(), result);
4859                result.push(child);
4860            }
4861            current = Box::leak(current.rest());
4862        }
4863    }
4864
4865    // Get input node or node-list
4866    if args.is_empty() {
4867        // No argument: use current node
4868        let node = match &arena.current_node {
4869            Some(n) => n.clone(),
4870            None => return Err("descendants: no current node".to_string()),
4871        };
4872        let mut descendants = Vec::new();
4873        collect_descendants(node.as_ref().as_ref(), &mut descendants);
4874        let node_list = crate::grove::VecNodeList::new(descendants);
4875        return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(node_list)))));
4876    }
4877
4878    // One argument: can be node-list or node
4879    match arena.get(args[0]) {
4880        ValueData::NodeList(nl) => {
4881            // Node-list input: collect descendants of all nodes
4882            let mut all_descendants = Vec::new();
4883            let mut current = nl.as_ref().as_ref();
4884            while !current.is_empty() {
4885                if let Some(node) = current.first() {
4886                    collect_descendants(node.as_ref(), &mut all_descendants);
4887                }
4888                current = Box::leak(current.rest());
4889            }
4890            let result = crate::grove::VecNodeList::new(all_descendants);
4891            Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(result)))))
4892        }
4893        ValueData::Node(n) => {
4894            // Single node input
4895            let mut descendants = Vec::new();
4896            collect_descendants(n.as_ref().as_ref(), &mut descendants);
4897            let node_list = crate::grove::VecNodeList::new(descendants);
4898            Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(node_list)))))
4899        }
4900        _ => Err("descendants: argument must be a node or node-list".to_string()),
4901    }
4902}
4903
4904/// follow - get all nodes following this node in document order
4905pub fn follow(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4906    if args.len() > 1 {
4907        return Err("follow: expected 0 or 1 arguments".to_string());
4908    }
4909
4910    let _node = if args.is_empty() {
4911        match &arena.current_node {
4912            Some(n) => n.clone(),
4913            None => return Err("follow: no current node".to_string()),
4914        }
4915    } else {
4916        match arena.get(args[0]) {
4917            ValueData::Node(n) => n.clone(),
4918            _ => return Err("follow: argument must be a node".to_string()),
4919        }
4920    };
4921
4922    // Stub: return empty node list
4923    // Full implementation requires document order traversal state
4924    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))))
4925}
4926
4927/// preced - get all nodes preceding this node in document order
4928pub fn preced(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4929    if args.len() > 1 {
4930        return Err("preced: expected 0 or 1 arguments".to_string());
4931    }
4932
4933    let _node = if args.is_empty() {
4934        match &arena.current_node {
4935            Some(n) => n.clone(),
4936            None => return Err("preced: no current node".to_string()),
4937        }
4938    } else {
4939        match arena.get(args[0]) {
4940            ValueData::Node(n) => n.clone(),
4941            _ => return Err("preced: argument must be a node".to_string()),
4942        }
4943    };
4944
4945    // Stub: return empty node list
4946    // Full implementation requires document order traversal state
4947    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))))
4948}
4949
4950/// ipreced - get immediately preceding node in document order
4951pub fn ipreced(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4952    if args.len() > 1 {
4953        return Err("ipreced: expected 0 or 1 arguments".to_string());
4954    }
4955
4956    let _node = if args.is_empty() {
4957        match &arena.current_node {
4958            Some(n) => n.clone(),
4959            None => return Err("ipreced: no current node".to_string()),
4960        }
4961    } else {
4962        match arena.get(args[0]) {
4963            ValueData::Node(n) => n.clone(),
4964            _ => return Err("ipreced: argument must be a node".to_string()),
4965        }
4966    };
4967
4968    // Stub: return empty node list
4969    // Full implementation requires document order traversal state
4970    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))))
4971}
4972
4973// ============================================================================
4974// Phase 3 Batch 33: Node-list set operations (5)
4975// ============================================================================
4976
4977/// node-list-last - get the last node in a node-list
4978pub fn node_list_last(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
4979    if args.len() != 1 {
4980        return Err("node-list-last: expected 1 argument".to_string());
4981    }
4982
4983    match arena.get(args[0]) {
4984        ValueData::NodeList(nl) => {
4985            let mut last_node = None;
4986            let mut current = nl.as_ref().as_ref();
4987            while !current.is_empty() {
4988                if let Some(node) = current.first() {
4989                    last_node = Some(node);
4990                }
4991                current = Box::leak(current.rest());
4992            }
4993            match last_node {
4994                Some(node) => Ok(arena.alloc(ValueData::Node(Rc::new(node)))),
4995                None => Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList))))),
4996            }
4997        }
4998        _ => Err("node-list-last: argument must be a node-list".to_string()),
4999    }
5000}
5001
5002/// node-list-union - combine two node-lists, removing duplicates
5003pub fn node_list_union(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5004    if args.is_empty() {
5005        return Err("node-list-union: expected at least 1 argument".to_string());
5006    }
5007
5008    // Collect all nodes from all argument lists, removing duplicates
5009    let mut nodes: Vec<Box<dyn crate::grove::Node>> = Vec::new();
5010
5011    for (i, arg_id) in args.iter().enumerate() {
5012        let nl = match arena.get(*arg_id) {
5013            ValueData::NodeList(nl) => nl.clone(),
5014            _ => return Err(format!("node-list-union: argument {} must be a node-list", i + 1)),
5015        };
5016
5017        // Use length-based iteration to avoid Box::leak memory issues
5018        let len = nl.length();
5019        for idx in 0..len {
5020            if let Some(node) = nl.get(idx) {
5021                // Check if this node is already in the list using node identity
5022                let mut already_present = false;
5023                for existing_node in &nodes {
5024                    if node.node_eq(existing_node.as_ref()) {
5025                        already_present = true;
5026                        break;
5027                    }
5028                }
5029                if !already_present {
5030                    nodes.push(node);
5031                }
5032            }
5033        }
5034    }
5035
5036    // Duplicates have been removed based on node identity
5037    let union = crate::grove::VecNodeList::new(nodes);
5038    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(union)))))
5039}
5040
5041/// node-list-intersection - get nodes present in both node-lists
5042pub fn node_list_intersection(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5043    if args.len() != 2 {
5044        return Err("node-list-intersection: expected 2 arguments".to_string());
5045    }
5046
5047    let _nl1 = match arena.get(args[0]) {
5048        ValueData::NodeList(nl) => nl.clone(),
5049        _ => return Err("node-list-intersection: first argument must be a node-list".to_string()),
5050    };
5051
5052    let _nl2 = match arena.get(args[1]) {
5053        ValueData::NodeList(nl) => nl.clone(),
5054        _ => return Err("node-list-intersection: second argument must be a node-list".to_string()),
5055    };
5056
5057    // Stub: return empty node list
5058    // Full implementation requires node identity comparison
5059    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList)))))
5060}
5061
5062/// node-list-difference - get nodes in first list but not in second
5063pub fn node_list_difference(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5064    if args.len() != 2 {
5065        return Err("node-list-difference: expected 2 arguments".to_string());
5066    }
5067
5068    let nl1 = match arena.get(args[0]) {
5069        ValueData::NodeList(nl) => nl.clone(),
5070        _ => return Err("node-list-difference: first argument must be a node-list".to_string()),
5071    };
5072
5073    let _nl2 = match arena.get(args[1]) {
5074        ValueData::NodeList(nl) => nl.clone(),
5075        _ => return Err("node-list-difference: second argument must be a node-list".to_string()),
5076    };
5077
5078    // Stub: return first list unchanged
5079    // Full implementation requires node identity comparison
5080    Ok(arena.alloc(ValueData::NodeList(nl1)))
5081}
5082
5083/// node-list-remove-duplicates - remove duplicate nodes from a node-list
5084pub fn node_list_remove_duplicates(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5085    if args.len() != 1 {
5086        return Err("node-list-remove-duplicates: expected 1 argument".to_string());
5087    }
5088
5089    let nl = match arena.get(args[0]) {
5090        ValueData::NodeList(nl) => nl.clone(),
5091        _ => return Err("node-list-remove-duplicates: argument must be a node-list".to_string()),
5092    };
5093
5094    // Stub: return list unchanged
5095    // Full implementation requires node identity tracking
5096    Ok(arena.alloc(ValueData::NodeList(nl)))
5097}
5098
5099// ============================================================================
5100// Phase 3 Batch 34: Element selection and position operations (5)
5101// ============================================================================
5102
5103/// select-elements - select elements by name from children
5104/// OpenJade signature: (select-elements node-list pattern)
5105pub fn select_elements(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5106    if args.len() != 2 {
5107        return Err("select-elements: expected 2 arguments".to_string());
5108    }
5109
5110    // OpenJade: args[0] is node-list, args[1] is pattern
5111    let nl = match arena.get(args[0]) {
5112        ValueData::NodeList(nl) => nl.clone(),
5113        _ => return Err("select-elements: first argument must be a node-list".to_string()),
5114    };
5115
5116    // Get the element name to select (second argument)
5117    let element_name = match arena.get(args[1]) {
5118        ValueData::String(s) => s.clone(),
5119        ValueData::Symbol(s) => s.to_string(),
5120        _ => return Err("select-elements: second argument must be a string or symbol".to_string()),
5121    };
5122
5123    // Filter elements by name
5124    let mut selected = Vec::new();
5125    let mut current = nl.as_ref().as_ref();
5126    while !current.is_empty() {
5127        if let Some(node) = current.first() {
5128            if let Some(gi) = node.gi() {
5129                if gi == element_name {
5130                    selected.push(node);
5131                }
5132            }
5133        }
5134        current = Box::leak(current.rest());
5135    }
5136
5137    let result = crate::grove::VecNodeList::new(selected);
5138    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(result)))))
5139}
5140
5141/// first-sibling? - check if node is the first sibling
5142pub fn first_sibling_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5143    if args.len() > 1 {
5144        return Err("first-sibling?: expected 0 or 1 arguments".to_string());
5145    }
5146
5147    let node = if args.is_empty() {
5148        match &arena.current_node {
5149            Some(n) => n.clone(),
5150            None => return Err("first-sibling?: no current node".to_string()),
5151        }
5152    } else {
5153        match arena.get(args[0]) {
5154            ValueData::Node(n) => n.clone(),
5155            _ => return Err("first-sibling?: argument must be a node".to_string()),
5156        }
5157    };
5158
5159    // OpenJade semantics: Check if node is first among siblings WITH SAME ELEMENT NAME
5160    // Not the first among all siblings, but first among siblings with matching GI
5161    let gi = node.gi();
5162
5163    match node.parent() {
5164        Some(parent) => {
5165            let children = parent.children();
5166            let len = children.length();
5167
5168            // Iterate through siblings until we find the current node
5169            for idx in 0..len {
5170                if let Some(sibling) = children.get(idx) {
5171                    // Check if this sibling has the same element name
5172                    if sibling.gi() == gi {
5173                        // If we found a sibling with same name, check if it's the current node
5174                        if node.node_eq(sibling.as_ref()) {
5175                            // Current node is the first with this element name
5176                            return Ok(crate::scheme::arena::TRUE_ID);
5177                        } else {
5178                            // Found an earlier sibling with same element name
5179                            return Ok(crate::scheme::arena::FALSE_ID);
5180                        }
5181                    }
5182                }
5183            }
5184            // Shouldn't reach here, but return false as fallback
5185            Ok(crate::scheme::arena::FALSE_ID)
5186        }
5187        None => Ok(crate::scheme::arena::TRUE_ID), // Root node is first sibling
5188    }
5189}
5190
5191/// last-sibling? - check if node is the last sibling
5192pub fn last_sibling_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5193    if args.len() > 1 {
5194        return Err("last-sibling?: expected 0 or 1 arguments".to_string());
5195    }
5196
5197    let node = if args.is_empty() {
5198        match &arena.current_node {
5199            Some(n) => n.clone(),
5200            None => return Err("last-sibling?: no current node".to_string()),
5201        }
5202    } else {
5203        match arena.get(args[0]) {
5204            ValueData::Node(n) => n.clone(),
5205            _ => return Err("last-sibling?: argument must be a node".to_string()),
5206        }
5207    };
5208
5209    // OpenJade semantics: Check if node is last among siblings WITH SAME ELEMENT NAME
5210    // Not the last among all siblings, but last among siblings with matching GI
5211    let gi = node.gi();
5212
5213    match node.parent() {
5214        Some(parent) => {
5215            let children = parent.children();
5216            let len = children.length();
5217
5218            // Iterate backwards through siblings to find the last with same element name
5219            for idx in (0..len).rev() {
5220                if let Some(sibling) = children.get(idx) {
5221                    // Check if this sibling has the same element name
5222                    if sibling.gi() == gi {
5223                        // Found the last sibling with this element name
5224                        // Check if it's the current node
5225                        if node.node_eq(sibling.as_ref()) {
5226                            return Ok(crate::scheme::arena::TRUE_ID);
5227                        } else {
5228                            return Ok(crate::scheme::arena::FALSE_ID);
5229                        }
5230                    }
5231                }
5232            }
5233            // Shouldn't reach here, but return false as fallback
5234            Ok(crate::scheme::arena::FALSE_ID)
5235        }
5236        None => Ok(crate::scheme::arena::TRUE_ID), // Root node is last sibling
5237    }
5238}
5239
5240/// child-number - get the child index of a node (1-based)
5241pub fn child_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5242    if args.len() > 1 {
5243        return Err("child-number: expected 0 or 1 arguments".to_string());
5244    }
5245
5246    let node = if args.is_empty() {
5247        match &arena.current_node {
5248            Some(n) => n.clone(),
5249            None => return Err("child-number: no current node".to_string()),
5250        }
5251    } else {
5252        match arena.get(args[0]) {
5253            ValueData::Node(n) => n.clone(),
5254            _ => return Err("child-number: argument must be a node".to_string()),
5255        }
5256    };
5257
5258    // OpenJade semantics: Count position among siblings WITH SAME ELEMENT NAME (1-based)
5259    // Like first-sibling? and last-sibling?, this counts only same-named siblings
5260    let gi = node.gi();
5261
5262    match node.parent() {
5263        Some(parent) => {
5264            let children = parent.children();
5265            let len = children.length();
5266            let mut position = 0;
5267
5268            // Iterate through siblings and count same-named ones until we find current node
5269            for idx in 0..len {
5270                if let Some(sibling) = children.get(idx) {
5271                    // Only count siblings with same element name
5272                    if sibling.gi() == gi {
5273                        position += 1;
5274                        // Check if this is the current node
5275                        if node.node_eq(sibling.as_ref()) {
5276                            return Ok(arena.int(position as i64));
5277                        }
5278                    }
5279                }
5280            }
5281            // Shouldn't reach here, but return 1 as fallback
5282            Ok(arena.int(1))
5283        }
5284        None => Ok(arena.int(1)), // Root node
5285    }
5286}
5287
5288/// element-with-id - find element with specified ID
5289pub fn element_with_id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5290    if args.len() != 1 {
5291        return Err("element-with-id: expected 1 argument".to_string());
5292    }
5293
5294    let id = match arena.get(args[0]) {
5295        ValueData::String(s) => s.clone(),
5296        ValueData::Symbol(s) => s.to_string(),
5297        _ => return Err("element-with-id: argument must be a string or symbol".to_string()),
5298    };
5299
5300    // Use grove's element_with_id if available
5301    match &arena.grove {
5302        Some(grove) => {
5303            match grove.element_with_id(&id) {
5304                // OpenJade: return singleton node-list, not bare node
5305                Some(node) => {
5306                    let singleton = crate::grove::VecNodeList::new(vec![node]);
5307                    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(singleton)))))
5308                }
5309                None => Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList))))),
5310            }
5311        }
5312        None => Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(crate::grove::EmptyNodeList))))),
5313    }
5314}
5315
5316// ============================================================================
5317// Phase 3 Batch 35: Element numbering operations (3)
5318// ============================================================================
5319
5320/// element-number - get the element number (count of same-named siblings)
5321pub fn element_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5322    if args.len() > 1 {
5323        return Err("element-number: expected 0 or 1 arguments".to_string());
5324    }
5325
5326    let _node = if args.is_empty() {
5327        match &arena.current_node {
5328            Some(n) => n.clone(),
5329            None => return Err("element-number: no current node".to_string()),
5330        }
5331    } else {
5332        match arena.get(args[0]) {
5333            ValueData::Node(n) => n.clone(),
5334            _ => return Err("element-number: argument must be a node".to_string()),
5335        }
5336    };
5337
5338    // Stub: return 1
5339    // Full implementation requires counting preceding siblings with same GI
5340    Ok(arena.int(1))
5341}
5342
5343/// hierarchical-number - get hierarchical numbering (e.g., 1.2.3)
5344pub fn hierarchical_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5345    if args.len() > 1 {
5346        return Err("hierarchical-number: expected 0 or 1 arguments".to_string());
5347    }
5348
5349    let _node = if args.is_empty() {
5350        match &arena.current_node {
5351            Some(n) => n.clone(),
5352            None => return Err("hierarchical-number: no current node".to_string()),
5353        }
5354    } else {
5355        match arena.get(args[0]) {
5356            ValueData::Node(n) => n.clone(),
5357            _ => return Err("hierarchical-number: argument must be a node".to_string()),
5358        }
5359    };
5360
5361    // Stub: return list (1)
5362    // Full implementation requires walking up tree and counting at each level
5363    let one = arena.int(1);
5364    Ok(arena.cons(one, crate::scheme::arena::NIL_ID))
5365}
5366
5367/// hierarchical-number-recursive - get recursive hierarchical numbering
5368pub fn hierarchical_number_recursive(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5369    if args.len() < 1 || args.len() > 2 {
5370        return Err("hierarchical-number-recursive: expected 1 or 2 arguments".to_string());
5371    }
5372
5373    // First arg is the element name or list of element names
5374    let _element_names = args[0];
5375
5376    let _node = if args.len() == 2 {
5377        match arena.get(args[1]) {
5378            ValueData::Node(n) => n.clone(),
5379            _ => return Err("hierarchical-number-recursive: second argument must be a node".to_string()),
5380        }
5381    } else {
5382        match &arena.current_node {
5383            Some(n) => n.clone(),
5384            None => return Err("hierarchical-number-recursive: no current node".to_string()),
5385        }
5386    };
5387
5388    // Stub: return list (1)
5389    let one = arena.int(1);
5390    Ok(arena.cons(one, crate::scheme::arena::NIL_ID))
5391}
5392
5393// Helper functions for number formatting
5394fn int_to_roman(mut n: i32, uppercase: bool) -> String {
5395    if n <= 0 || n > 3999 {
5396        return n.to_string();
5397    }
5398
5399    let values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
5400    let symbols_upper = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
5401    let symbols_lower = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
5402
5403    let symbols = if uppercase { &symbols_upper } else { &symbols_lower };
5404
5405    let mut result = String::new();
5406    for (i, &value) in values.iter().enumerate() {
5407        while n >= value {
5408            result.push_str(symbols[i]);
5409            n -= value;
5410        }
5411    }
5412    result
5413}
5414
5415fn int_to_alpha(n: i32, uppercase: bool) -> String {
5416    if n <= 0 {
5417        return n.to_string();
5418    }
5419
5420    let base = if uppercase { b'A' } else { b'a' };
5421    let mut result = String::new();
5422    let mut num = n - 1;
5423
5424    loop {
5425        result.insert(0, (base + (num % 26) as u8) as char);
5426        num /= 26;
5427        if num == 0 {
5428            break;
5429        }
5430        num -= 1;
5431    }
5432
5433    result
5434}
5435
5436// ============================================================================
5437// Phase 3 Batch 36: Grove utility operations (5)
5438// ============================================================================
5439
5440/// ancestors - get all ancestors of a node
5441pub fn ancestors(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5442    if args.len() > 1 {
5443        return Err("ancestors: expected 0 or 1 arguments".to_string());
5444    }
5445
5446    let node = if args.is_empty() {
5447        match &arena.current_node {
5448            Some(n) => n.clone(),
5449            None => return Err("ancestors: no current node".to_string()),
5450        }
5451    } else {
5452        // OpenJade optSingletonNodeList pattern: accept node, singleton node-list, or #f
5453        match arena.get(args[0]) {
5454            ValueData::Node(n) => n.clone(),
5455            ValueData::NodeList(nl) => {
5456                if nl.is_empty() {
5457                    // Empty node-list -> return empty node-list
5458                    return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(
5459                        crate::grove::VecNodeList::new(Vec::new())
5460                    )))));
5461                }
5462                let first = nl.first();
5463                let rest = nl.rest();
5464                if rest.is_empty() {
5465                    // Singleton node-list -> extract the node
5466                    Rc::new(first.unwrap())
5467                } else {
5468                    // Multi-element node-list -> error
5469                    return Err("ancestors: node-list must be a singleton".to_string());
5470                }
5471            }
5472            // OpenJade: #f returns empty node-list
5473            _ => return Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(
5474                crate::grove::VecNodeList::new(Vec::new())
5475            ))))),
5476        }
5477    };
5478
5479    // Collect all ancestors walking up the tree
5480    let mut ancestors = Vec::new();
5481    let mut current = node.parent();
5482    while let Some(parent_node) = current {
5483        ancestors.push(parent_node.clone_node());
5484        current = parent_node.parent();
5485    }
5486
5487    let node_list = crate::grove::VecNodeList::new(ancestors);
5488    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(node_list)))))
5489}
5490
5491/// document-element - get the document root element
5492pub fn document_element(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5493    if !args.is_empty() {
5494        return Err("document-element: expected no arguments".to_string());
5495    }
5496
5497    match &arena.grove {
5498        Some(grove) => {
5499            let root = grove.root();
5500            Ok(arena.alloc(ValueData::Node(Rc::new(root))))
5501        }
5502        None => Err("document-element: no grove available".to_string()),
5503    }
5504}
5505
5506/// have-ancestor? - check if node has ancestor with specified name
5507pub fn have_ancestor_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5508    if args.is_empty() || args.len() > 2 {
5509        return Err("have-ancestor?: expected 1 or 2 arguments".to_string());
5510    }
5511
5512    let ancestor_name = match arena.get(args[0]) {
5513        ValueData::String(s) => s.clone(),
5514        ValueData::Symbol(s) => s.to_string(),
5515        _ => return Err("have-ancestor?: first argument must be a string or symbol".to_string()),
5516    };
5517
5518    let node = if args.len() == 2 {
5519        match arena.get(args[1]) {
5520            ValueData::Node(n) => n.clone(),
5521            _ => return Err("have-ancestor?: second argument must be a node".to_string()),
5522        }
5523    } else {
5524        match &arena.current_node {
5525            Some(n) => n.clone(),
5526            None => return Err("have-ancestor?: no current node".to_string()),
5527        }
5528    };
5529
5530    // Walk up tree looking for matching ancestor
5531    let mut current = node.parent();
5532    while let Some(parent_node) = current {
5533        if let Some(gi) = parent_node.gi() {
5534            if gi == ancestor_name {
5535                return Ok(crate::scheme::arena::TRUE_ID);
5536            }
5537        }
5538        current = parent_node.parent();
5539    }
5540
5541    Ok(crate::scheme::arena::FALSE_ID)
5542}
5543
5544/// match-element? - check if node matches element name
5545pub fn match_element_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5546    if args.is_empty() || args.len() > 2 {
5547        return Err("match-element?: expected 1 or 2 arguments".to_string());
5548    }
5549
5550    let element_name = match arena.get(args[0]) {
5551        ValueData::String(s) => s.clone(),
5552        ValueData::Symbol(s) => s.to_string(),
5553        _ => return Err("match-element?: first argument must be a string or symbol".to_string()),
5554    };
5555
5556    let node = if args.len() == 2 {
5557        match arena.get(args[1]) {
5558            ValueData::Node(n) => n.clone(),
5559            _ => return Err("match-element?: second argument must be a node".to_string()),
5560        }
5561    } else {
5562        match &arena.current_node {
5563            Some(n) => n.clone(),
5564            None => return Err("match-element?: no current node".to_string()),
5565        }
5566    };
5567
5568    match node.gi() {
5569        Some(gi) if gi == element_name => Ok(crate::scheme::arena::TRUE_ID),
5570        _ => Ok(crate::scheme::arena::FALSE_ID),
5571    }
5572}
5573
5574/// node-list-map - map a function over a node-list (stub)
5575pub fn node_list_map(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5576    if args.len() != 2 {
5577        return Err("node-list-map: expected 2 arguments".to_string());
5578    }
5579
5580    let _func = args[0];
5581    let nl = match arena.get(args[1]) {
5582        ValueData::NodeList(nl) => nl.clone(),
5583        _ => return Err("node-list-map: second argument must be a node-list".to_string()),
5584    };
5585
5586    // Stub: return the node-list unchanged
5587    // Full implementation requires calling the function on each node
5588    Ok(arena.alloc(ValueData::NodeList(nl)))
5589}
5590
5591// ============================================================================
5592// Phase 3 Batch 37: Final DSSSL operations (5)
5593// ============================================================================
5594
5595/// node-property - get a property value from a node (stub)
5596pub fn node_property(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5597    if args.len() < 1 || args.len() > 2 {
5598        return Err("node-property: expected 1 or 2 arguments".to_string());
5599    }
5600
5601    let _property_name = match arena.get(args[0]) {
5602        ValueData::String(s) => s.clone(),
5603        ValueData::Symbol(s) => s.to_string(),
5604        _ => return Err("node-property: first argument must be a string or symbol".to_string()),
5605    };
5606
5607    let _node = if args.len() == 2 {
5608        match arena.get(args[1]) {
5609            ValueData::Bool(false) => {
5610                // #f means no node - return #f
5611                return Ok(crate::scheme::arena::FALSE_ID);
5612            }
5613            ValueData::Node(n) => n.clone(),
5614            ValueData::NodeList(nl) => {
5615                // Accept singleton node-lists (DSSSL pattern: current-node returns singleton)
5616                if nl.is_empty() {
5617                    return Ok(crate::scheme::arena::FALSE_ID);
5618                }
5619                let first = nl.first();
5620                let rest = nl.rest();
5621                if rest.is_empty() {
5622                    // Singleton node-list -> extract the node
5623                    Rc::new(first.unwrap())
5624                } else {
5625                    // Multi-element node-list -> error
5626                    return Err("node-property: node-list must be a singleton".to_string());
5627                }
5628            }
5629            _ => {
5630                return Err("node-property: second argument must be a node, singleton node-list, or #f".to_string());
5631            }
5632        }
5633    } else {
5634        match &arena.current_node {
5635            Some(n) => n.clone(),
5636            None => return Err("node-property: no current node".to_string()),
5637        }
5638    };
5639
5640    // Stub: return #f
5641    // Full implementation would access node properties dynamically
5642    Ok(crate::scheme::arena::FALSE_ID)
5643}
5644
5645/// absolute-first-sibling? - check if node is first sibling at document level
5646pub fn absolute_first_sibling_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5647    if args.len() > 1 {
5648        return Err("absolute-first-sibling?: expected 0 or 1 arguments".to_string());
5649    }
5650
5651    let _node = if args.is_empty() {
5652        match &arena.current_node {
5653            Some(n) => n.clone(),
5654            None => return Err("absolute-first-sibling?: no current node".to_string()),
5655        }
5656    } else {
5657        match arena.get(args[0]) {
5658            ValueData::Node(n) => n.clone(),
5659            _ => return Err("absolute-first-sibling?: argument must be a node".to_string()),
5660        }
5661    };
5662
5663    // Stub: return #f
5664    // Full implementation requires comparing node identity with first sibling
5665    Ok(crate::scheme::arena::FALSE_ID)
5666}
5667
5668/// absolute-last-sibling? - check if node is last sibling at document level
5669pub fn absolute_last_sibling_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5670    if args.len() > 1 {
5671        return Err("absolute-last-sibling?: expected 0 or 1 arguments".to_string());
5672    }
5673
5674    let _node = if args.is_empty() {
5675        match &arena.current_node {
5676            Some(n) => n.clone(),
5677            None => return Err("absolute-last-sibling?: no current node".to_string()),
5678        }
5679    } else {
5680        match arena.get(args[0]) {
5681            ValueData::Node(n) => n.clone(),
5682            _ => return Err("absolute-last-sibling?: argument must be a node".to_string()),
5683        }
5684    };
5685
5686    // Stub: return #f
5687    // Full implementation requires comparing node identity with last sibling
5688    Ok(crate::scheme::arena::FALSE_ID)
5689}
5690
5691/// node-list->list - convert a node-list to a list
5692pub fn node_list_to_list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5693    if args.len() != 1 {
5694        return Err("node-list->list: expected 1 argument".to_string());
5695    }
5696
5697    let nl = match arena.get(args[0]) {
5698        ValueData::NodeList(nl) => nl.clone(),
5699        _ => return Err("node-list->list: argument must be a node-list".to_string()),
5700    };
5701
5702    // Convert node-list to list
5703    let mut nodes = Vec::new();
5704    let mut current = nl.as_ref().as_ref();
5705    while !current.is_empty() {
5706        if let Some(node) = current.first() {
5707            let node_id = arena.alloc(ValueData::Node(Rc::new(node)));
5708            nodes.push(node_id);
5709        }
5710        current = Box::leak(current.rest());
5711    }
5712
5713    // Build list from nodes
5714    let mut result = crate::scheme::arena::NIL_ID;
5715    for node_id in nodes.iter().rev() {
5716        result = arena.cons(*node_id, result);
5717    }
5718
5719    Ok(result)
5720}
5721
5722/// node-list-contains? - check if node-list contains a node
5723pub fn node_list_contains_p(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5724    if args.len() != 2 {
5725        return Err("node-list-contains?: expected 2 arguments".to_string());
5726    }
5727
5728    let nl = match arena.get(args[0]) {
5729        ValueData::NodeList(nl) => nl.clone(),
5730        _ => return Err("node-list-contains?: first argument must be a node-list".to_string()),
5731    };
5732
5733    // OpenJade: optSingletonNodeList pattern - accept node or singleton node-list
5734    let target_node = match arena.get(args[1]) {
5735        ValueData::Node(n) => n.clone(),
5736        ValueData::NodeList(nl) => {
5737            if nl.is_empty() {
5738                // Empty node-list -> return #f (nothing to search for)
5739                return Ok(crate::scheme::arena::FALSE_ID);
5740            }
5741            let first = nl.first();
5742            let rest = nl.rest();
5743            if rest.is_empty() {
5744                // Singleton node-list -> extract the node
5745                Rc::new(first.unwrap())
5746            } else {
5747                // Multi-element node-list -> error
5748                return Err("node-list-contains?: second argument (node-list) must be a singleton".to_string());
5749            }
5750        }
5751        _ => return Err("node-list-contains?: second argument must be a node or node-list".to_string()),
5752    };
5753
5754    // Check if target node is in the node-list
5755    // Convert to vec and iterate
5756    let mut nodes = Vec::new();
5757    let mut current_nl = nl.clone();
5758    loop {
5759        if current_nl.is_empty() {
5760            break;
5761        }
5762        nodes.push(current_nl.first().unwrap());
5763        current_nl = Rc::new(current_nl.rest());
5764    }
5765
5766    // Check each node for equality
5767    for node in nodes {
5768        if node.as_ref().node_eq(&**target_node) {
5769            return Ok(crate::scheme::arena::TRUE_ID);
5770        }
5771    }
5772
5773    Ok(crate::scheme::arena::FALSE_ID)
5774}
5775
5776// ============================================================================
5777// Phase 3 Batch 38: Entity and notation operations (5)
5778// ============================================================================
5779
5780/// entity-system-id - get the system identifier of an entity
5781pub fn entity_system_id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5782    if args.is_empty() {
5783        return Err("entity-system-id: requires at least 1 argument".to_string());
5784    }
5785
5786    // Extract entity name
5787    let _entity_name = match arena.get(args[0]) {
5788        ValueData::String(s) => s.clone(),
5789        ValueData::Symbol(s) => s.to_string(),
5790        _ => return Err("entity-system-id: argument must be a string or symbol".to_string()),
5791    };
5792
5793    // Stub: In a full implementation, this would query the DTD for entity declarations
5794    // and return the system identifier. For now, return #f (not found).
5795    Ok(crate::scheme::arena::FALSE_ID)
5796}
5797
5798/// entity-public-id - get the public identifier of an entity
5799pub fn entity_public_id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5800    if args.is_empty() {
5801        return Err("entity-public-id: requires at least 1 argument".to_string());
5802    }
5803
5804    // Extract entity name
5805    let _entity_name = match arena.get(args[0]) {
5806        ValueData::String(s) => s.clone(),
5807        ValueData::Symbol(s) => s.to_string(),
5808        _ => return Err("entity-public-id: argument must be a string or symbol".to_string()),
5809    };
5810
5811    // Stub: In a full implementation, this would query the DTD for entity declarations
5812    // and return the public identifier. For now, return #f (not found).
5813    Ok(crate::scheme::arena::FALSE_ID)
5814}
5815
5816/// entity-type - get the type of an entity
5817pub fn entity_type(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5818    if args.is_empty() {
5819        return Err("entity-type: requires at least 1 argument".to_string());
5820    }
5821
5822    // Extract entity name
5823    let _entity_name = match arena.get(args[0]) {
5824        ValueData::String(s) => s.clone(),
5825        ValueData::Symbol(s) => s.to_string(),
5826        _ => return Err("entity-type: argument must be a string or symbol".to_string()),
5827    };
5828
5829    // Stub: In a full implementation, this would return a symbol indicating the entity type:
5830    // 'text, 'cdata, 'sdata, 'ndata, 'subdoc, or 'pi
5831    // For now, return #f (not found).
5832    Ok(crate::scheme::arena::FALSE_ID)
5833}
5834
5835/// notation-system-id - get the system identifier of a notation
5836pub fn notation_system_id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5837    if args.is_empty() {
5838        return Err("notation-system-id: requires at least 1 argument".to_string());
5839    }
5840
5841    // Extract notation name
5842    let _notation_name = match arena.get(args[0]) {
5843        ValueData::String(s) => s.clone(),
5844        ValueData::Symbol(s) => s.to_string(),
5845        _ => return Err("notation-system-id: argument must be a string or symbol".to_string()),
5846    };
5847
5848    // Stub: In a full implementation, this would query the DTD for notation declarations
5849    // and return the system identifier. For now, return #f (not found).
5850    Ok(crate::scheme::arena::FALSE_ID)
5851}
5852
5853/// notation-public-id - get the public identifier of a notation
5854pub fn notation_public_id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5855    if args.is_empty() {
5856        return Err("notation-public-id: requires at least 1 argument".to_string());
5857    }
5858
5859    // Extract notation name
5860    let _notation_name = match arena.get(args[0]) {
5861        ValueData::String(s) => s.clone(),
5862        ValueData::Symbol(s) => s.to_string(),
5863        _ => return Err("notation-public-id: argument must be a string or symbol".to_string()),
5864    };
5865
5866    // Stub: In a full implementation, this would query the DTD for notation declarations
5867    // and return the public identifier. For now, return #f (not found).
5868    Ok(crate::scheme::arena::FALSE_ID)
5869}
5870
5871// ============================================================================
5872// Phase 3 Batch 39: Context and debugging primitives (5)
5873// ============================================================================
5874
5875/// current-language - get the current language code
5876pub fn current_language(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
5877    if !args.is_empty() {
5878        return Err("current-language: requires 0 arguments".to_string());
5879    }
5880
5881    // Stub: In a full implementation, this would return the current language code
5882    // set by with-language or declare-default-language.
5883    // For now, return #f (no language set).
5884    Ok(crate::scheme::arena::FALSE_ID)
5885}
5886
5887/// current-mode - get the current processing mode
5888pub fn current_mode(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
5889    if !args.is_empty() {
5890        return Err("current-mode: requires 0 arguments".to_string());
5891    }
5892
5893    // Stub: In a full implementation, this would return a symbol representing
5894    // the current processing mode set by with-mode.
5895    // For now, return #f (no mode set).
5896    Ok(crate::scheme::arena::FALSE_ID)
5897}
5898
5899/// current-node-address - get the address of the current node
5900pub fn current_node_address(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
5901    if !args.is_empty() {
5902        return Err("current-node-address: requires 0 arguments".to_string());
5903    }
5904
5905    // Stub: In a full implementation, this would return an address object
5906    // representing the position of the current node in the grove.
5907    // For now, return #f (no address available).
5908    Ok(crate::scheme::arena::FALSE_ID)
5909}
5910
5911/// current-node-page-number-sosofo - get page number sosofo for current node
5912pub fn current_node_page_number_sosofo(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5913    if !args.is_empty() {
5914        return Err("current-node-page-number-sosofo: requires 0 arguments".to_string());
5915    }
5916
5917    // Stub: In a full implementation, this would return a sosofo that expands
5918    // to the page number where the current node appears.
5919    // For now, return an empty sosofo.
5920    Ok(arena.alloc(ValueData::Sosofo))
5921}
5922
5923/// debug - print debug information (output to stderr)
5924pub fn debug(arena: &Arena, args: &[ValueId]) -> ArenaResult {
5925    // Print all arguments to stderr for debugging
5926    eprint!("[DEBUG]");
5927    for arg_id in args {
5928        match arena.get(*arg_id) {
5929            ValueData::Integer(n) => eprint!(" {}", n),
5930            ValueData::Real(f) => eprint!(" {}", f),
5931            ValueData::String(s) => eprint!(" \"{}\"", s),
5932            ValueData::Symbol(s) => eprint!(" {}", s),
5933            ValueData::Bool(b) => eprint!(" {}", if *b { "#t" } else { "#f" }),
5934            ValueData::Char(c) => eprint!(" #\\{}", c),
5935            ValueData::Nil => eprint!(" ()"),
5936            ValueData::Pair { .. } => eprint!(" <pair>"),
5937            ValueData::Vector(_) => eprint!(" <vector>"),
5938            ValueData::Node(_) => eprint!(" <node>"),
5939            ValueData::NodeList(_) => eprint!(" <node-list>"),
5940            ValueData::Sosofo => eprint!(" <sosofo>"),
5941            ValueData::Unspecified => eprint!(" <unspecified>"),
5942            ValueData::Error => eprint!(" <error>"),
5943            ValueData::Procedure(_) => eprint!(" <procedure>"),
5944            ValueData::Quantity { magnitude, .. } => eprint!(" {}pt", magnitude),
5945            ValueData::Keyword(k) => eprint!(" #:{}", k),
5946        }
5947    }
5948    eprintln!();
5949
5950    // Return unspecified
5951    Ok(crate::scheme::arena::UNSPECIFIED_ID)
5952}
5953
5954// Phase 3 Batch 41: Type predicates and error (3 primitives)
5955
5956/// exact? - Check if number is exact (integers are exact)
5957pub fn exact_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
5958    if args.len() != 1 {
5959        return Err(format!("exact?: expected 1 argument, got {}", args.len()));
5960    }
5961
5962    match arena.get(args[0]) {
5963        ValueData::Integer(_) => Ok(crate::scheme::arena::TRUE_ID),
5964        ValueData::Real(_) => Ok(crate::scheme::arena::FALSE_ID),
5965        ValueData::Quantity { .. } => Ok(crate::scheme::arena::FALSE_ID),
5966        _ => Err("exact?: argument must be a number".to_string()),
5967    }
5968}
5969
5970/// inexact? - Check if number is inexact (floats are inexact)
5971pub fn inexact_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
5972    if args.len() != 1 {
5973        return Err(format!("inexact?: expected 1 argument, got {}", args.len()));
5974    }
5975
5976    match arena.get(args[0]) {
5977        ValueData::Integer(_) => Ok(crate::scheme::arena::FALSE_ID),
5978        ValueData::Real(_) => Ok(crate::scheme::arena::TRUE_ID),
5979        ValueData::Quantity { .. } => Ok(crate::scheme::arena::TRUE_ID),
5980        _ => Err("inexact?: argument must be a number".to_string()),
5981    }
5982}
5983
5984/// error - Signal an error
5985pub fn error(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
5986    let msg = if args.is_empty() {
5987        "error".to_string()
5988    } else {
5989        // Build error message from arguments
5990        let mut parts = Vec::new();
5991        for arg in args {
5992            match arena.get(*arg) {
5993                ValueData::String(s) => parts.push(s.clone()),
5994                ValueData::Symbol(s) => parts.push(s.to_string()),
5995                ValueData::Integer(n) => parts.push(n.to_string()),
5996                ValueData::Real(f) => parts.push(f.to_string()),
5997                ValueData::Bool(b) => parts.push(if *b { "#t" } else { "#f" }.to_string()),
5998                ValueData::Char(c) => parts.push(format!("#\\{}", c)),
5999                _ => parts.push("<value>".to_string()),
6000            }
6001        }
6002        parts.join(" ")
6003    };
6004
6005    Err(msg)
6006}
6007
6008// Phase 3 Batch 42: Address type stubs (3 primitives)
6009// These are stubs - addresses not implemented yet
6010
6011/// address? - Check if value is an address (stub: always false)
6012pub fn address_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6013    if args.len() != 1 {
6014        return Err(format!("address?: expected 1 argument, got {}", args.len()));
6015    }
6016    // Addresses not implemented yet, always return false
6017    Ok(crate::scheme::arena::FALSE_ID)
6018}
6019
6020/// address-local? - Check if address is local (stub: always false)
6021pub fn address_local_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6022    if args.len() != 1 {
6023        return Err(format!("address-local?: expected 1 argument, got {}", args.len()));
6024    }
6025    // Addresses not implemented yet, always return false
6026    Ok(crate::scheme::arena::FALSE_ID)
6027}
6028
6029/// address-visited? - Check if address has been visited (stub: always false)
6030pub fn address_visited_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6031    if args.len() != 1 {
6032        return Err(format!("address-visited?: expected 1 argument, got {}", args.len()));
6033    }
6034    // Addresses not implemented yet, always return false
6035    Ok(crate::scheme::arena::FALSE_ID)
6036}
6037
6038// Phase 3 Batch 43: Color/display space stubs (4 primitives)
6039// These are stubs - full color/space types not implemented yet
6040
6041/// color-space? - Check if value is a color-space (stub: always false)
6042pub fn color_space_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6043    if args.len() != 1 {
6044        return Err(format!("color-space?: expected 1 argument, got {}", args.len()));
6045    }
6046    // Color-spaces not implemented yet, always return false
6047    Ok(crate::scheme::arena::FALSE_ID)
6048}
6049
6050/// color-space - Return a color-space (stub: return unspecified)
6051pub fn color_space(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6052    // Stub: return unspecified
6053    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6054}
6055
6056/// display-space - Return a display-space (stub: return unspecified)
6057pub fn display_space(_arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6058    // Stub: return unspecified
6059    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6060}
6061
6062/// inline-space - Return an inline-space (stub: return unspecified)
6063pub fn inline_space(_arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6064    // Stub: return unspecified
6065    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6066}
6067
6068// Phase 3 Batch 44: Glyph type stubs (5 primitives)
6069// These are stubs - glyph types not implemented yet
6070
6071/// glyph-id? - Check if value is a glyph-id (stub: always false)
6072pub fn glyph_id_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6073    if args.len() != 1 {
6074        return Err(format!("glyph-id?: expected 1 argument, got {}", args.len()));
6075    }
6076    // Glyph-ids not implemented yet, always return false
6077    Ok(crate::scheme::arena::FALSE_ID)
6078}
6079
6080/// glyph-id - Return a glyph-id (stub: return unspecified)
6081pub fn glyph_id(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6082    // Stub: return unspecified
6083    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6084}
6085
6086/// glyph-subst-table? - Check if value is a glyph-subst-table (stub: always false)
6087pub fn glyph_subst_table_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6088    if args.len() != 1 {
6089        return Err(format!("glyph-subst-table?: expected 1 argument, got {}", args.len()));
6090    }
6091    // Glyph-subst-tables not implemented yet, always return false
6092    Ok(crate::scheme::arena::FALSE_ID)
6093}
6094
6095/// glyph-subst-table - Return a glyph-subst-table (stub: return unspecified)
6096pub fn glyph_subst_table(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6097    // Stub: return unspecified
6098    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6099}
6100
6101/// glyph-subst - Return a glyph substitution (stub: return unspecified)
6102pub fn glyph_subst(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6103    // Stub: return unspecified
6104    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6105}
6106
6107// Phase 3 Batch 45: Time type stubs (6 primitives)
6108// These are stubs - time type not implemented yet
6109
6110/// time - Return current time (stub: return unspecified)
6111pub fn time(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6112    // Stub: return unspecified
6113    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6114}
6115
6116/// time->string - Convert time to string (stub: return empty string)
6117pub fn time_to_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6118    if args.len() != 1 {
6119        return Err(format!("time->string: expected 1 argument, got {}", args.len()));
6120    }
6121    // Stub: return empty string
6122    Ok(arena.string(String::new()))
6123}
6124
6125/// time<=? - Time less-than-or-equal comparison (stub: always false)
6126pub fn time_le(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6127    if args.len() != 2 {
6128        return Err(format!("time<=?: expected 2 arguments, got {}", args.len()));
6129    }
6130    // Stub: always false
6131    Ok(crate::scheme::arena::FALSE_ID)
6132}
6133
6134/// time<? - Time less-than comparison (stub: always false)
6135pub fn time_lt(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6136    if args.len() != 2 {
6137        return Err(format!("time<?: expected 2 arguments, got {}", args.len()));
6138    }
6139    // Stub: always false
6140    Ok(crate::scheme::arena::FALSE_ID)
6141}
6142
6143/// time>=? - Time greater-than-or-equal comparison (stub: always false)
6144pub fn time_ge(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6145    if args.len() != 2 {
6146        return Err(format!("time>=?: expected 2 arguments, got {}", args.len()));
6147    }
6148    // Stub: always false
6149    Ok(crate::scheme::arena::FALSE_ID)
6150}
6151
6152/// time>? - Time greater-than comparison (stub: always false)
6153pub fn time_gt(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6154    if args.len() != 2 {
6155        return Err(format!("time>?: expected 2 arguments, got {}", args.len()));
6156    }
6157    // Stub: always false
6158    Ok(crate::scheme::arena::FALSE_ID)
6159}
6160
6161// Phase 3 Batch 46: Language and style type stubs (3 primitives)
6162// These are stubs - language/style types not fully implemented yet
6163
6164/// language? - Check if value is a language (stub: always false)
6165pub fn language_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6166    if args.len() != 1 {
6167        return Err(format!("language?: expected 1 argument, got {}", args.len()));
6168    }
6169    // Language type not implemented yet, always return false
6170    Ok(crate::scheme::arena::FALSE_ID)
6171}
6172
6173/// language - Return language (stub: return "en" for English)
6174/// In DSSSL, this returns the language of a node or the current language.
6175/// For now, we return "en" (English) as a reasonable default.
6176pub fn language(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6177    // Return "en" (English) as default language
6178    // DocBook stylesheets expect language codes like "en", "de", "fr", etc.
6179    Ok(arena.symbol("en".into()))
6180}
6181
6182/// style? - Check if value is a style (stub: always false)
6183pub fn style_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6184    if args.len() != 1 {
6185        return Err(format!("style?: expected 1 argument, got {}", args.len()));
6186    }
6187    // Style type not implemented yet, always return false
6188    Ok(crate::scheme::arena::FALSE_ID)
6189}
6190
6191// Phase 3 Batch 47: String comparison and simple stubs (3 primitives)
6192
6193/// string-equiv? - String equivalence comparison (case-insensitive)
6194pub fn string_equiv_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
6195    if args.len() != 2 {
6196        return Err(format!("string-equiv?: expected 2 arguments, got {}", args.len()));
6197    }
6198
6199    let s1 = match arena.get(args[0]) {
6200        ValueData::String(s) => s.to_lowercase(),
6201        _ => return Err("string-equiv?: first argument must be a string".to_string()),
6202    };
6203
6204    let s2 = match arena.get(args[1]) {
6205        ValueData::String(s) => s.to_lowercase(),
6206        _ => return Err("string-equiv?: second argument must be a string".to_string()),
6207    };
6208
6209    Ok(if s1 == s2 {
6210        crate::scheme::arena::TRUE_ID
6211    } else {
6212        crate::scheme::arena::FALSE_ID
6213    })
6214}
6215
6216/// label-length - Return label length (stub: return 0)
6217pub fn label_length(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6218    // Stub: return 0
6219    Ok(arena.int(0))
6220}
6221
6222/// external-procedure - Look up external procedure by name and return it as a symbol
6223/// The evaluator will recognize this symbol and dispatch to the correct primitive
6224pub fn external_procedure(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6225    use crate::scheme::arena::ValueData;
6226    use std::rc::Rc;
6227
6228    if args.len() != 1 {
6229        return Err("external-procedure: requires 1 argument".to_string());
6230    }
6231
6232    // Get the external procedure name string
6233    let name_str = match arena.get(args[0]) {
6234        ValueData::String(s) => s.clone(),
6235        _ => return Err("external-procedure: argument must be a string".to_string()),
6236    };
6237
6238    // Parse the external procedure name
6239    // Format: "UNREGISTERED::James Clark//Procedure::procedure-name"
6240    let proc_name = if let Some(pos) = name_str.rfind("::") {
6241        &name_str[pos + 2..]
6242    } else {
6243        &name_str
6244    };
6245
6246    // Validate that the procedure exists
6247    let valid = matches!(proc_name,
6248        // Flow object constructors
6249        "if-first-page" | "if-front-page" | "empty-sosofo" | "literal" |
6250        "sosofo-append" | "page-number-sosofo" | "current-node-page-number-sosofo" |
6251        // Processing
6252        "process-children" | "process-node-list" | "next-match" |
6253        // Grove queries
6254        "current-node" | "node-list-first" | "node-list-rest" | "node-list-length" |
6255        "empty-node-list" | "children" | "parent" | "ancestor" | "descendants" |
6256        "follow" | "preced" | "attributes" | "gi" | "id" | "data" |
6257        "attribute-string" | "inherited-attribute-string"
6258    );
6259
6260    if !valid {
6261        return Err(format!("external-procedure: unknown procedure '{}'", proc_name));
6262    }
6263
6264    // Return a symbol with the primitive name - the evaluator will handle dispatch
6265    let symbol_data = ValueData::Symbol(Rc::from(proc_name));
6266    Ok(arena.alloc(symbol_data))
6267}
6268
6269// Phase 3 Batch 48: DTD/SGML stubs (6 primitives)
6270// These are stubs - DTD/SGML operations not fully implemented
6271
6272/// declaration - Return declaration (stub: return unspecified)
6273pub fn declaration(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6274    // Stub: DTD declarations not implemented
6275    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6276}
6277
6278/// dtd - Return DTD (stub: return unspecified)
6279pub fn dtd(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6280    // Stub: DTD access not implemented
6281    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6282}
6283
6284/// epilog - Return epilog (stub: return empty node-list)
6285pub fn epilog(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6286    // Stub: return empty node-list
6287    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6288}
6289
6290/// prolog - Return prolog (stub: return empty node-list)
6291pub fn prolog(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6292    // Stub: return empty node-list
6293    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6294}
6295
6296/// sgml-declaration - Return SGML declaration (stub: return unspecified)
6297pub fn sgml_declaration(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6298    // Stub: SGML declarations not implemented
6299    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6300}
6301
6302/// sgml-parse - Parse SGML (stub: return unspecified)
6303pub fn sgml_parse(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6304    // Stub: SGML parsing not implemented
6305    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6306}
6307
6308// Phase 3 Batch 49: Entity/normalization stubs (4 primitives)
6309// These are stubs - entity operations not fully implemented
6310
6311/// entity-address - Return entity address (stub: return unspecified)
6312pub fn entity_address(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6313    // Stub: entity addresses not implemented
6314    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6315}
6316
6317/// entity-generated-system-id - Return generated system ID (stub: return empty string)
6318pub fn entity_generated_system_id(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6319    if args.len() != 1 {
6320        return Err(format!("entity-generated-system-id: expected 1 argument, got {}", args.len()));
6321    }
6322    // Stub: return empty string
6323    Ok(arena.string(String::new()))
6324}
6325
6326/// entity-name-normalize - Normalize entity name (stub: return same string)
6327pub fn entity_name_normalize(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6328    if args.len() != 1 {
6329        return Err(format!("entity-name-normalize: expected 1 argument, got {}", args.len()));
6330    }
6331    // Stub: just return the input string
6332    Ok(args[0])
6333}
6334
6335/// general-name-normalize - Normalize general name (stub: return same string)
6336/// (general-name-normalize name [node]) → string
6337/// The second argument (node) is optional and ignored in XML processing
6338pub fn general_name_normalize(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6339    if args.is_empty() || args.len() > 2 {
6340        return Err(format!("general-name-normalize: expected 1 or 2 arguments, got {}", args.len()));
6341    }
6342    // Stub: just return the input string (ignore optional node argument)
6343    Ok(args[0])
6344}
6345
6346/// normalize - Normalize name (alias for general-name-normalize)
6347/// (normalize name [node]) → string
6348pub fn normalize(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6349    // Just delegate to general-name-normalize
6350    general_name_normalize(_arena, args)
6351}
6352
6353// Phase 3 Batch 50: Simple navigation and declaration stubs (5 primitives)
6354
6355/// first-child-gi - Return first child GI (stub: return empty string)
6356pub fn first_child_gi(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6357    if args.len() != 1 {
6358        return Err(format!("first-child-gi: expected 1 argument, got {}", args.len()));
6359    }
6360    // Stub: return empty string
6361    Ok(arena.string(String::new()))
6362}
6363
6364/// tree-root - Return tree root
6365pub fn tree_root(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6366    // Optimization: Use Grove's root() method directly instead of traversing parent chain
6367    // This is much faster for large documents with deep nesting
6368
6369    if let Some(ref grove) = arena.grove {
6370        // Fast path: Get root directly from Grove
6371        let root = grove.root();
6372        Ok(arena.alloc(ValueData::Node(Rc::new(root))))
6373    } else {
6374        // Fallback: Traverse parent chain if no Grove available
6375        // Get the node to start from
6376        let start_node = if args.is_empty() {
6377            // No argument: use current node
6378            match &arena.current_node {
6379                Some(n) => n.clone(),
6380                None => return Ok(crate::scheme::arena::UNSPECIFIED_ID),
6381            }
6382        } else {
6383            // With argument: optSingletonNodeList pattern
6384            match arena.get(args[0]) {
6385                ValueData::Node(n) => n.clone(),
6386                ValueData::NodeList(nl) => {
6387                    if nl.is_empty() {
6388                        return Ok(crate::scheme::arena::UNSPECIFIED_ID);
6389                    }
6390                    let first = nl.first();
6391                    let rest = nl.rest();
6392                    if rest.is_empty() {
6393                        Rc::new(first.unwrap())
6394                    } else {
6395                        return Err("tree-root: node-list must be a singleton".to_string());
6396                    }
6397                }
6398                _ => return Ok(crate::scheme::arena::UNSPECIFIED_ID),
6399            }
6400        };
6401
6402        // Traverse up the tree to find the root element
6403        let mut current = start_node;
6404        loop {
6405            match current.parent() {
6406                Some(parent) => {
6407                    // Continue traversing up
6408                    current = Rc::new(parent);
6409                }
6410                None => {
6411                    // No parent - this is the root
6412                    break;
6413                }
6414            }
6415        }
6416
6417        Ok(arena.alloc(ValueData::Node(current)))
6418    }
6419}
6420
6421/// declare-default-language - Declare default language (stub: return unspecified)
6422pub fn declare_default_language(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6423    // Stub: language declarations not implemented
6424    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6425}
6426
6427/// read-entity - Read entity (stub: return empty string)
6428pub fn read_entity(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6429    // Stub: entity reading not implemented, return empty string
6430    Ok(arena.string(String::new()))
6431}
6432
6433/// set-visited! - Set visited flag (stub: return unspecified)
6434pub fn set_visited(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6435    // Stub: visited tracking not implemented
6436    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6437}
6438
6439// Phase 3 Batch 51: Sosofo and navigation stubs (4 primitives)
6440
6441/// sosofo-contains-node? - Check if sosofo contains node (stub: always false)
6442pub fn sosofo_contains_node_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6443    if args.len() != 2 {
6444        return Err(format!("sosofo-contains-node?: expected 2 arguments, got {}", args.len()));
6445    }
6446    // Stub: always return false
6447    Ok(crate::scheme::arena::FALSE_ID)
6448}
6449
6450/// page-number-sosofo - Return page number sosofo (stub: return empty sosofo)
6451pub fn page_number_sosofo(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6452    // Stub: page numbering not fully implemented, return empty sosofo
6453    // TODO: Implement actual page numbering
6454    Ok(arena.alloc(ValueData::Sosofo))
6455}
6456
6457/// ifollow - Inverse follow navigation (stub: return empty node-list)
6458pub fn ifollow(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6459    // Stub: inverse navigation not implemented, return empty node-list
6460    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6461}
6462
6463/// with-language - Execute with language (stub: evaluate body)
6464pub fn with_language(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6465    // Stub: language switching not implemented
6466    // Just return unspecified since we can't evaluate the body here
6467    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6468}
6469
6470// Phase 3 Batch 52: Element numbering stubs (3 primitives)
6471
6472/// all-element-number - Return all element number (stub: return 0)
6473pub fn all_element_number(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6474    // Stub: element numbering not implemented, return 0
6475    Ok(arena.int(0))
6476}
6477
6478/// ancestor-child-number - Return ancestor child number (stub: return 0)
6479pub fn ancestor_child_number(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6480    if args.len() != 2 {
6481        return Err(format!("ancestor-child-number: expected 2 arguments, got {}", args.len()));
6482    }
6483    // Stub: element numbering not implemented, return 0
6484    Ok(arena.int(0))
6485}
6486
6487/// element-number-list - Return element number list (stub: return empty list)
6488pub fn element_number_list(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6489    // Stub: element numbering not implemented, return empty list
6490    Ok(crate::scheme::arena::NIL_ID)
6491}
6492
6493// Phase 3 Batch 53: Inherited property stubs - Part 1 (5 primitives)
6494
6495/// inherited-attribute-string - Get inherited attribute value (stub: return #f)
6496pub fn inherited_attribute_string(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6497    if args.is_empty() || args.len() > 2 {
6498        return Err(format!("inherited-attribute-string: expected 1-2 arguments, got {}", args.len()));
6499    }
6500    // Stub: inherited attribute lookup not implemented, return #f
6501    Ok(crate::scheme::arena::FALSE_ID)
6502}
6503
6504/// inherited-element-attribute-string - Get inherited element attribute (stub: return empty string)
6505pub fn inherited_element_attribute_string(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6506    if args.len() < 1 || args.len() > 2 {
6507        return Err(format!("inherited-element-attribute-string: expected 1 or 2 arguments, got {}", args.len()));
6508    }
6509    // Stub: inherited element attribute lookup not implemented, return empty string
6510    Ok(arena.string(String::new()))
6511}
6512
6513/// inherited-start-indent - Get inherited start indent (stub: return 0)
6514pub fn inherited_start_indent(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6515    // Stub: inherited indent not implemented, return 0
6516    Ok(arena.int(0))
6517}
6518
6519/// inherited-end-indent - Get inherited end indent (stub: return 0)
6520pub fn inherited_end_indent(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6521    // Stub: inherited indent not implemented, return 0
6522    Ok(arena.int(0))
6523}
6524
6525/// inherited-line-spacing - Get inherited line spacing (stub: return 0)
6526pub fn inherited_line_spacing(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6527    // Stub: inherited line spacing not implemented, return 0
6528    Ok(arena.int(0))
6529}
6530
6531// Phase 3 Batch 54: Inherited property stubs - Part 2 (6 primitives)
6532
6533/// inherited-font-family-name - Get inherited font family (stub: return empty string)
6534pub fn inherited_font_family_name(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6535    // Stub: inherited font family not implemented, return empty string
6536    Ok(arena.string(String::new()))
6537}
6538
6539/// inherited-font-size - Get inherited font size (stub: return 12)
6540pub fn inherited_font_size(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6541    // Stub: inherited font size not implemented, return 12pt
6542    Ok(arena.int(12))
6543}
6544
6545/// inherited-font-weight - Get inherited font weight (stub: return empty string)
6546pub fn inherited_font_weight(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6547    // Stub: inherited font weight not implemented, return empty string
6548    Ok(arena.string(String::new()))
6549}
6550
6551/// inherited-font-posture - Get inherited font posture (stub: return empty string)
6552pub fn inherited_font_posture(arena: &mut Arena, _args: &[ValueId]) -> ArenaResult {
6553    // Stub: inherited font posture not implemented, return empty string
6554    Ok(arena.string(String::new()))
6555}
6556
6557/// inherited-dbhtml-value - Get inherited dbhtml value (stub: return empty string)
6558pub fn inherited_dbhtml_value(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6559    if args.len() != 1 {
6560        return Err(format!("inherited-dbhtml-value: expected 1 argument, got {}", args.len()));
6561    }
6562    // Stub: inherited dbhtml value not implemented, return empty string
6563    Ok(arena.string(String::new()))
6564}
6565
6566/// inherited-pi-value - Get inherited PI value (stub: return empty string)
6567pub fn inherited_pi_value(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6568    if args.len() != 1 {
6569        return Err(format!("inherited-pi-value: expected 1 argument, got {}", args.len()));
6570    }
6571    // Stub: inherited PI value not implemented, return empty string
6572    Ok(arena.string(String::new()))
6573}
6574
6575// Phase 3 Batch 55: Node-list operation stubs - Part 1 (5 primitives)
6576
6577/// node-list - Create node list from nodes and node-lists
6578/// (node-list node-or-list ...) → node-list
6579/// Concatenates all arguments (nodes and node-lists) into a single node-list
6580pub fn node_list(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6581    use crate::grove::VecNodeList;
6582
6583    let mut result_nodes = Vec::new();
6584
6585    for arg in args {
6586        match arena.get(*arg) {
6587            ValueData::Node(node) => {
6588                // Single node: add it to the result
6589                result_nodes.push(node.as_ref().clone_node());
6590            }
6591            ValueData::NodeList(nl) => {
6592                // Node-list: add all its nodes to the result
6593                let len = nl.length();
6594                for i in 0..len {
6595                    if let Some(node) = nl.get(i) {
6596                        result_nodes.push(node);
6597                    }
6598                }
6599            }
6600            _ => {
6601                return Err("node-list: arguments must be nodes or node-lists".to_string());
6602            }
6603        }
6604    }
6605
6606    Ok(arena.alloc(ValueData::NodeList(Rc::new(Box::new(VecNodeList::new(result_nodes))))))
6607}
6608
6609/// node-list=? - Compare node lists for equality
6610/// Returns #t if both node-lists contain the same nodes in the same order, #f otherwise
6611pub fn node_list_eq_p(arena: &Arena, args: &[ValueId]) -> ArenaResult {
6612    use crate::scheme::arena::{FALSE_ID, TRUE_ID};
6613
6614    if args.len() != 2 {
6615        return Err(format!("node-list=?: expected 2 arguments, got {}", args.len()));
6616    }
6617
6618    // Quick check: if both arguments are the same ValueId, they're equal
6619    if args[0] == args[1] {
6620        return Ok(TRUE_ID);
6621    }
6622
6623    // Get both arguments - they can be nodes, node-lists, or #f
6624    let nl1 = match arena.get(args[0]) {
6625        ValueData::Bool(false) => {
6626            // #f -> treat as empty node-list
6627            vec![]
6628        }
6629        ValueData::Node(n) => {
6630            // Single node -> treat as singleton node-list
6631            vec![n.clone()]
6632        }
6633        ValueData::NodeList(nl) => {
6634            // Collect all nodes from the node-list
6635            let mut nodes = Vec::new();
6636            let mut index = 0;
6637            while let Some(node) = nl.get(index) {
6638                nodes.push(std::rc::Rc::new(node));
6639                index += 1;
6640            }
6641            nodes
6642        }
6643        _ => return Err("node-list=?: first argument must be a node, node-list, or #f".to_string()),
6644    };
6645
6646    let nl2 = match arena.get(args[1]) {
6647        ValueData::Bool(false) => {
6648            // #f -> treat as empty node-list
6649            vec![]
6650        }
6651        ValueData::Node(n) => {
6652            // Single node -> treat as singleton node-list
6653            vec![n.clone()]
6654        }
6655        ValueData::NodeList(nl) => {
6656            // Collect all nodes from the node-list
6657            let mut nodes = Vec::new();
6658            let mut index = 0;
6659            while let Some(node) = nl.get(index) {
6660                nodes.push(std::rc::Rc::new(node));
6661                index += 1;
6662            }
6663            nodes
6664        }
6665        v => {
6666            let type_name = match v {
6667                ValueData::Nil => "empty list",
6668                ValueData::Bool(_) => "boolean",
6669                ValueData::Integer(_) => "integer",
6670                ValueData::Real(_) => "real",
6671                ValueData::Char(_) => "char",
6672                ValueData::String(_) => "string",
6673                ValueData::Symbol(_) => "symbol",
6674                ValueData::Keyword(_) => "keyword",
6675                ValueData::Pair { .. } => "pair",
6676                ValueData::Vector(_) => "vector",
6677                ValueData::Procedure(_) => "procedure",
6678                ValueData::Quantity { .. } => "quantity",
6679                ValueData::Sosofo => "sosofo",
6680                ValueData::Unspecified => "unspecified",
6681                ValueData::Error => "error",
6682                _ => "unknown",
6683            };
6684            return Err(format!("node-list=?: second argument must be a node, node-list, or #f (got {})", type_name));
6685        }
6686    };
6687
6688    // Compare lengths first
6689    if nl1.len() != nl2.len() {
6690        return Ok(FALSE_ID);
6691    }
6692
6693    // Compare nodes element by element
6694    // OpenJade uses pointer equality (*nd1 != *nd2)
6695    // In Rust, we use node_eq() which compares the underlying node identity
6696    // (not the Rc wrapper address, but the actual XML node identity)
6697    for (node1, node2) in nl1.iter().zip(nl2.iter()) {
6698        // Compare node identity - nodes are equal if they refer to the same XML node
6699        // node1 and node2 are both Rc<Box<dyn Node>>, so we need to dereference to &dyn Node
6700        if !node1.as_ref().as_ref().node_eq(node2.as_ref().as_ref()) {
6701            return Ok(FALSE_ID);
6702        }
6703    }
6704
6705    Ok(TRUE_ID)
6706}
6707
6708/// node-list-count - Count nodes matching predicate (stub: return 0)
6709pub fn node_list_count(arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6710    if args.len() != 1 {
6711        return Err(format!("node-list-count: expected 1 argument, got {}", args.len()));
6712    }
6713    // Implementation: node-list-length(node-list-remove-duplicates(nl))
6714    let unique_nl = node_list_remove_duplicates(arena, args)?;
6715    node_list_length(arena, &[unique_nl])
6716}
6717
6718/// node-list-union-map - Union of mapped node lists (stub: return empty node list)
6719pub fn node_list_union_map(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6720    if args.len() != 2 {
6721        return Err(format!("node-list-union-map: expected 2 arguments, got {}", args.len()));
6722    }
6723    // Stub: node list union map not implemented, return empty node list
6724    Ok(crate::scheme::arena::NIL_ID)
6725}
6726
6727/// node-list-symmetrical-difference - Symmetric difference of node lists (stub: return empty node list)
6728pub fn node_list_symmetrical_difference(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6729    if args.len() != 2 {
6730        return Err(format!("node-list-symmetrical-difference: expected 2 arguments, got {}", args.len()));
6731    }
6732    // Stub: node list symmetric difference not implemented, return empty node list
6733    Ok(crate::scheme::arena::NIL_ID)
6734}
6735
6736// Phase 3 Batch 56: Node-list operation stubs - Part 2 (4 primitives)
6737
6738/// node-list-address - Get address of node in node list (stub: return empty list)
6739/// Takes 1-2 arguments: (node-list-address nl [snl])
6740pub fn node_list_address(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6741    if args.is_empty() || args.len() > 2 {
6742        return Err(format!("node-list-address: expected 1-2 arguments, got {}", args.len()));
6743    }
6744    // Stub: node list address not implemented, return empty list
6745    Ok(crate::scheme::arena::NIL_ID)
6746}
6747
6748/// node-list-error - Create error node list (stub: return empty node list)
6749pub fn node_list_error(_arena: &mut Arena, args: &[ValueId]) -> ArenaResult {
6750    if args.len() != 1 {
6751        return Err(format!("node-list-error: expected 1 argument, got {}", args.len()));
6752    }
6753    // Stub: node list error not implemented, return empty node list
6754    Ok(crate::scheme::arena::NIL_ID)
6755}
6756
6757/// node-list-no-order - Remove ordering from node list (stub: return empty node list)
6758pub fn node_list_no_order(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6759    if args.len() != 1 {
6760        return Err(format!("node-list-no-order: expected 1 argument, got {}", args.len()));
6761    }
6762    // Stub: node list no-order not implemented, return empty node list
6763    Ok(crate::scheme::arena::NIL_ID)
6764}
6765
6766/// origin-to-subnode-rel-forest-addr - Get relative forest address (stub: return empty list)
6767pub fn origin_to_subnode_rel_forest_addr(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6768    if args.len() != 1 {
6769        return Err(format!("origin-to-subnode-rel-forest-addr: expected 1 argument, got {}", args.len()));
6770    }
6771    // Stub: relative forest address not implemented, return empty list
6772    Ok(crate::scheme::arena::NIL_ID)
6773}
6774
6775// Phase 3 Batch 57: Named node list stubs (3 primitives)
6776
6777/// named-node - Get node by name (stub: return #f)
6778pub fn named_node(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6779    if args.len() != 1 {
6780        return Err(format!("named-node: expected 1 argument, got {}", args.len()));
6781    }
6782    // Stub: named nodes not implemented, return #f
6783    Ok(crate::scheme::arena::FALSE_ID)
6784}
6785
6786/// named-node-list? - Check if value is a named node list (stub: return #f)
6787pub fn named_node_list_p(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6788    if args.len() != 1 {
6789        return Err(format!("named-node-list?: expected 1 argument, got {}", args.len()));
6790    }
6791    // Stub: named node lists not implemented, return #f
6792    Ok(crate::scheme::arena::FALSE_ID)
6793}
6794
6795/// named-node-list-names - Get names from named node list (stub: return empty list)
6796pub fn named_node_list_names(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6797    if args.len() != 1 {
6798        return Err(format!("named-node-list-names: expected 1 argument, got {}", args.len()));
6799    }
6800    // Stub: named node list names not implemented, return empty list
6801    Ok(crate::scheme::arena::NIL_ID)
6802}
6803
6804// Phase 3 Batch 58: Selection operation stubs (2 primitives)
6805
6806/// select-by-class - Select nodes by class (stub: return empty node list)
6807pub fn select_by_class(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6808    if args.len() != 1 {
6809        return Err(format!("select-by-class: expected 1 argument, got {}", args.len()));
6810    }
6811    // Stub: select by class not implemented, return empty node list
6812    Ok(crate::scheme::arena::NIL_ID)
6813}
6814
6815/// select-children - Select child elements (stub: return empty node list)
6816pub fn select_children(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6817    // Takes variable args (element names to match)
6818    // Stub: select children not implemented, return empty node list
6819    Ok(crate::scheme::arena::NIL_ID)
6820}
6821
6822// Phase 3 Batch 59: Processing operation stubs (5 primitives)
6823
6824/// process-children-trim - Process children with whitespace trimming (stub: return unspecified)
6825pub fn process_children_trim(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6826    // Stub: process-children-trim not implemented, return unspecified
6827    // Note: This is a processing operation that would need evaluator integration
6828    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6829}
6830
6831/// process-element-with-id - Process element with given ID (stub: return unspecified)
6832pub fn process_element_with_id(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6833    if args.len() != 1 {
6834        return Err(format!("process-element-with-id: expected 1 argument, got {}", args.len()));
6835    }
6836    // Stub: process-element-with-id not implemented, return unspecified
6837    // Note: This would need grove integration to find element by ID
6838    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6839}
6840
6841/// process-first-descendant - Process first descendant matching predicate (stub: return unspecified)
6842pub fn process_first_descendant(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6843    if args.len() != 1 {
6844        return Err(format!("process-first-descendant: expected 1 argument, got {}", args.len()));
6845    }
6846    // Stub: process-first-descendant not implemented, return unspecified
6847    // Note: This would need grove traversal and evaluator integration
6848    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6849}
6850
6851/// process-matching-children - Process children matching predicate (stub: return unspecified)
6852pub fn process_matching_children(_arena: &Arena, args: &[ValueId]) -> ArenaResult {
6853    if args.len() != 2 {
6854        return Err(format!("process-matching-children: expected 2 arguments, got {}", args.len()));
6855    }
6856    // Stub: process-matching-children not implemented, return unspecified
6857    // Note: This would need evaluator integration for predicate evaluation
6858    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6859}
6860
6861/// next-match - Continue to next matching rule (stub: return unspecified)
6862pub fn next_match(_arena: &Arena, _args: &[ValueId]) -> ArenaResult {
6863    // Stub: next-match not implemented as primitive, return unspecified
6864    // Note: This is typically implemented as a special form in the evaluator
6865    Ok(crate::scheme::arena::UNSPECIFIED_ID)
6866}
6867
6868#[cfg(test)]
6869mod tests {
6870    use super::*;
6871
6872    #[test]
6873    fn test_car() {
6874        let mut arena = Arena::new();
6875        let one = arena.int(1);
6876        let two = arena.int(2);
6877        let pair = arena.cons(one, two);
6878
6879        let result = car(&arena, &[pair]).unwrap();
6880        assert_eq!(result, one);
6881    }
6882
6883    #[test]
6884    fn test_cdr() {
6885        let mut arena = Arena::new();
6886        let one = arena.int(1);
6887        let two = arena.int(2);
6888        let pair = arena.cons(one, two);
6889
6890        let result = cdr(&arena, &[pair]).unwrap();
6891        assert_eq!(result, two);
6892    }
6893
6894    #[test]
6895    fn test_cons() {
6896        let mut arena = Arena::new();
6897        let one = arena.int(1);
6898        let two = arena.int(2);
6899
6900        let pair = cons(&mut arena, &[one, two]).unwrap();
6901
6902        match arena.get(pair) {
6903            ValueData::Pair { car, cdr, .. } => {
6904                assert_eq!(*car, one);
6905                assert_eq!(*cdr, two);
6906            }
6907            _ => panic!("Expected pair"),
6908        }
6909    }
6910
6911    #[test]
6912    fn test_null() {
6913        let mut arena = Arena::new();
6914
6915        assert_eq!(
6916            null(&arena, &[NIL_ID]).unwrap(),
6917            crate::scheme::arena::TRUE_ID
6918        );
6919
6920        let one = arena.int(1);
6921        assert_eq!(
6922            null(&arena, &[one]).unwrap(),
6923            crate::scheme::arena::FALSE_ID
6924        );
6925    }
6926
6927    #[test]
6928    fn test_equal() {
6929        let mut arena = Arena::new();
6930
6931        // Equal integers
6932        let one1 = arena.int(1);
6933        let one2 = arena.int(1);
6934        assert_eq!(
6935            equal(&arena, &[one1, one2]).unwrap(),
6936            crate::scheme::arena::TRUE_ID
6937        );
6938
6939        // Unequal integers
6940        let two = arena.int(2);
6941        assert_eq!(
6942            equal(&arena, &[one1, two]).unwrap(),
6943            crate::scheme::arena::FALSE_ID
6944        );
6945
6946        // Equal lists
6947        let list1 = arena.cons(one1, NIL_ID);
6948        let list2 = arena.cons(one2, NIL_ID);
6949        assert_eq!(
6950            equal(&arena, &[list1, list2]).unwrap(),
6951            crate::scheme::arena::TRUE_ID
6952        );
6953    }
6954}