Skip to main content

vm/builtins/runtime/
core.rs

1use super::typed::{
2    VmArrayHandle, VmArrayRef, VmBytesHandle, VmBytesRef, VmMapHandle, VmMapRef, VmStringRef,
3    VmValueOwned, VmValueRef, take_arg,
4};
5use super::{AnyValue, UnknownValue, VmArray, VmBytes, VmMap, arg, return_one};
6use crate::bytecode::{SharedArray, SharedMap};
7use crate::vm::{CallReturn, Value, VmError, VmResult};
8use pd_host_function::pd_host_function;
9use rt_format::{Format, FormatArgument, NoNamedArguments, ParsedFormat, Specifier};
10use std::sync::Arc;
11
12/// Bind a callable prototype to an owned capture array.
13#[allow(dead_code)]
14#[pd_host_function(name = "__bind_callable")]
15fn builtin_bind_callable_metadata(_prototype_id: i64, captures: VmArrayRef<'_>) -> VmArray {
16    captures.to_vec()
17}
18
19/// Detach a moved local slot from any shared capture cell.
20#[allow(dead_code)]
21#[pd_host_function(name = "__detach_local")]
22fn builtin_detach_local_metadata(_slot: i64) {}
23
24/// Return the length of a string, array, or map.
25#[pd_host_function(name = "len")]
26pub(super) fn builtin_len_string_impl(text: VmStringRef<'_>) -> i64 {
27    text.chars().count() as i64
28}
29
30/// Return the length of an array.
31#[pd_host_function(name = "len")]
32pub(super) fn builtin_len_array_impl(items: VmArrayRef<'_>) -> i64 {
33    items.len() as i64
34}
35
36/// Return the length of a byte sequence.
37#[pd_host_function(name = "len")]
38pub(super) fn builtin_len_bytes_impl(items: VmBytesRef<'_>) -> i64 {
39    items.len() as i64
40}
41
42/// Return the number of entries in a map.
43#[pd_host_function(name = "len")]
44pub(super) fn builtin_len_map_impl(entries: VmMapRef<'_>) -> i64 {
45    entries.len() as i64
46}
47
48pub(super) fn builtin_len(args: &[Value]) -> VmResult<CallReturn> {
49    let value = arg::<&Value>(args, 0, "len value")?;
50    match value {
51        Value::String(text) => Ok(return_one(builtin_len_string_impl(text.as_str()))),
52        Value::Bytes(values) => Ok(return_one(values.len())),
53        Value::Array(values) => Ok(return_one(values.len())),
54        Value::Map(entries) => Ok(return_one(entries.len())),
55        _ => Err(VmError::TypeMismatch("string/bytes/array/map")),
56    }
57}
58
59fn slice_bounds(start: i64, length: i64) -> VmResult<Option<(usize, usize)>> {
60    if start < 0 || length <= 0 {
61        return Ok(None);
62    }
63    let start = usize::try_from(start).map_err(|_| {
64        VmError::HostError("slice start overflow while converting to usize".to_string())
65    })?;
66    let length = usize::try_from(length).map_err(|_| {
67        VmError::HostError("slice length overflow while converting to usize".to_string())
68    })?;
69    Ok(Some((start, length)))
70}
71
72/// Slice a string from the given start and length.
73#[pd_host_function(name = "slice")]
74pub(super) fn builtin_slice_string_impl(
75    text: VmStringRef<'_>,
76    start: i64,
77    length: i64,
78) -> VmResult<String> {
79    let Some((start, length)) = slice_bounds(start, length)? else {
80        return Ok(String::new());
81    };
82    Ok(text.chars().skip(start).take(length).collect::<String>())
83}
84
85/// Slice an array from the given start and length.
86#[pd_host_function(name = "slice")]
87pub(super) fn builtin_slice_array_impl(
88    items: VmArrayRef<'_>,
89    start: i64,
90    length: i64,
91) -> VmResult<VmArray> {
92    let Some((start, length)) = slice_bounds(start, length)? else {
93        return Ok(Vec::new());
94    };
95    Ok(items
96        .iter()
97        .skip(start)
98        .take(length)
99        .cloned()
100        .collect::<Vec<_>>())
101}
102
103/// Slice bytes from the given start and length.
104#[pd_host_function(name = "slice")]
105pub(super) fn builtin_slice_bytes_impl(
106    items: VmBytesRef<'_>,
107    start: i64,
108    length: i64,
109) -> VmResult<VmBytes> {
110    let Some((start, length)) = slice_bounds(start, length)? else {
111        return Ok(Vec::new());
112    };
113    Ok(items
114        .iter()
115        .skip(start)
116        .take(length)
117        .copied()
118        .collect::<Vec<_>>())
119}
120
121pub(super) fn builtin_slice(args: &[Value]) -> VmResult<CallReturn> {
122    let source = arg::<&Value>(args, 0, "slice source")?;
123    let start = arg::<i64>(args, 1, "slice start")?;
124    let length = arg::<i64>(args, 2, "slice length")?;
125    match source {
126        Value::String(text) => {
127            builtin_slice_string_impl(text.as_str(), start, length).map(return_one)
128        }
129        Value::Array(values) => {
130            let Some((start, length)) = slice_bounds(start, length)? else {
131                return Ok(return_one(Vec::<Value>::new()));
132            };
133            Ok(return_one(
134                values
135                    .iter()
136                    .skip(start)
137                    .take(length)
138                    .cloned()
139                    .collect::<Vec<_>>(),
140            ))
141        }
142        Value::Bytes(values) => {
143            let Some((start, length)) = slice_bounds(start, length)? else {
144                return Ok(return_one(Vec::<u8>::new()));
145            };
146            Ok(return_one(
147                values
148                    .iter()
149                    .skip(start)
150                    .take(length)
151                    .copied()
152                    .collect::<Vec<_>>(),
153            ))
154        }
155        _ => Err(VmError::TypeMismatch("string/bytes/array")),
156    }
157}
158
159/// Concatenate two strings.
160#[pd_host_function(name = "concat")]
161pub(super) fn builtin_concat_string_impl(left: VmStringRef<'_>, right: VmStringRef<'_>) -> String {
162    let mut out = String::with_capacity(left.len() + right.len());
163    out.push_str(left);
164    out.push_str(right);
165    out
166}
167
168/// Concatenate two arrays.
169#[pd_host_function(name = "concat")]
170pub(super) fn builtin_concat_array_impl(
171    mut left: VmArrayHandle,
172    right: VmArrayHandle,
173) -> VmArrayHandle {
174    Arc::make_mut(&mut left).extend(right.iter().cloned());
175    left
176}
177
178/// Concatenate two byte sequences.
179#[pd_host_function(name = "concat")]
180pub(super) fn builtin_concat_bytes_impl(
181    mut left: VmBytesHandle,
182    right: VmBytesHandle,
183) -> VmBytesHandle {
184    Arc::make_mut(&mut left).extend(right.iter().copied());
185    left
186}
187
188pub(super) fn builtin_concat(args: &[Value]) -> VmResult<CallReturn> {
189    let left = arg::<&Value>(args, 0, "concat left")?;
190    let right = arg::<&Value>(args, 1, "concat right")?;
191    match (left, right) {
192        (Value::String(left), Value::String(right)) => Ok(return_one(builtin_concat_string_impl(
193            left.as_str(),
194            right.as_str(),
195        ))),
196        (Value::Array(left), Value::Array(right)) => {
197            let mut values = Vec::with_capacity(left.len() + right.len());
198            values.extend(left.iter().cloned());
199            values.extend(right.iter().cloned());
200            Ok(return_one(values))
201        }
202        (Value::Bytes(left), Value::Bytes(right)) => {
203            let mut values = Vec::with_capacity(left.len() + right.len());
204            values.extend(left.iter().copied());
205            values.extend(right.iter().copied());
206            Ok(return_one(values))
207        }
208        _ => Err(VmError::TypeMismatch(
209            "string/string or bytes/bytes or array/array",
210        )),
211    }
212}
213
214/// Create an empty array.
215#[pd_host_function(name = "array_new")]
216pub(super) fn builtin_array_new_impl() -> VmArray {
217    Vec::new()
218}
219
220/// Append a value to an array and return the updated array.
221#[pd_host_function(name = "array_push")]
222pub(super) fn builtin_array_push_typed_impl(
223    mut items: VmArrayHandle,
224    value: VmValueOwned,
225) -> VmArrayHandle {
226    Arc::make_mut(&mut items).push(value);
227    items
228}
229
230pub(crate) fn builtin_array_push_shared_impl(
231    mut items: SharedArray,
232    value: AnyValue,
233) -> SharedArray {
234    Arc::make_mut(&mut items).push(value);
235    items
236}
237
238pub(crate) fn builtin_array_push_owned(items: Value, value: Value) -> VmResult<Value> {
239    let items = match items {
240        Value::Array(values) => values,
241        _ => return Err(VmError::TypeMismatch("array")),
242    };
243    Ok(Value::Array(builtin_array_push_shared_impl(items, value)))
244}
245
246pub(super) fn builtin_array_push(args: &mut [Value]) -> VmResult<CallReturn> {
247    let items = take_arg(args, 0, "array_push array")?;
248    let value = take_arg(args, 1, "array_push value")?;
249    builtin_array_push_owned(items, value).map(return_one)
250}
251
252/// Create an empty map.
253#[pd_host_function(name = "map_new")]
254pub(super) fn builtin_map_new_impl() -> VmMap {
255    VmMap::new()
256}
257
258/// Read a string entry.
259#[pd_host_function(name = "get")]
260pub(super) fn builtin_get_string_impl(text: VmStringRef<'_>, index: i64) -> VmResult<String> {
261    if index < 0 {
262        return Err(VmError::HostError(
263            "string index must be non-negative".to_string(),
264        ));
265    }
266    let index = usize::try_from(index)
267        .map_err(|_| VmError::HostError("string index overflow".to_string()))?;
268    text.chars()
269        .nth(index)
270        .map(|ch| ch.to_string())
271        .ok_or_else(|| VmError::HostError(format!("string index {index} out of bounds")))
272}
273
274/// Read an array element by index.
275#[pd_host_function(name = "get")]
276pub(super) fn builtin_get_array_impl(items: VmArrayRef<'_>, index: i64) -> VmResult<UnknownValue> {
277    if index < 0 {
278        return Err(VmError::HostError(
279            "array index must be non-negative".to_string(),
280        ));
281    }
282    let index = usize::try_from(index)
283        .map_err(|_| VmError::HostError("array index overflow".to_string()))?;
284    items
285        .get(index)
286        .cloned()
287        .ok_or_else(|| VmError::HostError(format!("array index {index} out of bounds")))
288}
289
290/// Read a byte value by index.
291#[pd_host_function(name = "get")]
292pub(super) fn builtin_get_bytes_impl(items: VmBytesRef<'_>, index: i64) -> VmResult<i64> {
293    if index < 0 {
294        return Err(VmError::HostError(
295            "bytes index must be non-negative".to_string(),
296        ));
297    }
298    let index = usize::try_from(index)
299        .map_err(|_| VmError::HostError("bytes index overflow".to_string()))?;
300    items
301        .get(index)
302        .copied()
303        .map(i64::from)
304        .ok_or_else(|| VmError::HostError(format!("bytes index {index} out of bounds")))
305}
306
307/// Read a map value by key.
308#[pd_host_function(name = "get")]
309pub(super) fn builtin_get_map_impl(
310    entries: VmMapRef<'_>,
311    key: VmValueRef<'_>,
312) -> VmResult<UnknownValue> {
313    entries
314        .get(key)
315        .cloned()
316        .ok_or_else(|| VmError::HostError("map key not found".to_string()))
317}
318
319/// Check whether an array contains a valid index.
320#[pd_host_function(name = "has")]
321pub(super) fn builtin_has_array_impl(items: VmArrayRef<'_>, index: i64) -> bool {
322    if index < 0 {
323        return false;
324    }
325    usize::try_from(index)
326        .ok()
327        .is_some_and(|index| index < items.len())
328}
329
330/// Check whether bytes contain a valid index.
331#[pd_host_function(name = "has")]
332pub(super) fn builtin_has_bytes_impl(items: VmBytesRef<'_>, index: i64) -> bool {
333    if index < 0 {
334        return false;
335    }
336    usize::try_from(index)
337        .ok()
338        .is_some_and(|index| index < items.len())
339}
340
341/// Check whether a map contains a key.
342#[pd_host_function(name = "has")]
343pub(super) fn builtin_has_map_impl(entries: VmMapRef<'_>, key: VmValueRef<'_>) -> bool {
344    entries.get(key).is_some()
345}
346
347pub(super) fn builtin_has(args: &[Value]) -> VmResult<CallReturn> {
348    let container = arg::<&Value>(args, 0, "has container")?;
349    let key = arg::<&Value>(args, 1, "has key")?;
350    match container {
351        Value::Array(values) => {
352            let index = key.as_int()?;
353            let present = if index < 0 {
354                false
355            } else {
356                usize::try_from(index)
357                    .ok()
358                    .is_some_and(|index| index < values.len())
359            };
360            Ok(return_one(present))
361        }
362        Value::Bytes(values) => {
363            let index = key.as_int()?;
364            let present = if index < 0 {
365                false
366            } else {
367                usize::try_from(index)
368                    .ok()
369                    .is_some_and(|index| index < values.len())
370            };
371            Ok(return_one(present))
372        }
373        Value::Map(entries) => {
374            ensure_supported_map_key(key)?;
375            Ok(return_one(entries.get(key).is_some()))
376        }
377        _ => Err(VmError::TypeMismatch("bytes/array/map")),
378    }
379}
380
381pub(super) fn builtin_get(args: &[Value]) -> VmResult<CallReturn> {
382    let container = arg::<&Value>(args, 0, "get container")?;
383    let key = arg::<&Value>(args, 1, "get key")?;
384    match container {
385        Value::Array(values) => {
386            let index = key.as_int()?;
387            if index < 0 {
388                return Err(VmError::HostError(
389                    "array index must be non-negative".to_string(),
390                ));
391            }
392            let index = usize::try_from(index)
393                .map_err(|_| VmError::HostError("array index overflow".to_string()))?;
394            Ok(return_one(values.get(index).cloned().ok_or_else(|| {
395                VmError::HostError(format!("array index {index} out of bounds"))
396            })?))
397        }
398        Value::Map(entries) => {
399            ensure_supported_map_key(key)?;
400            Ok(return_one(entries.get(key).cloned().ok_or_else(|| {
401                VmError::HostError("map key not found".to_string())
402            })?))
403        }
404        Value::Bytes(values) => {
405            let index = key.as_int()?;
406            if index < 0 {
407                return Err(VmError::HostError(
408                    "bytes index must be non-negative".to_string(),
409                ));
410            }
411            let index = usize::try_from(index)
412                .map_err(|_| VmError::HostError("bytes index overflow".to_string()))?;
413            Ok(return_one(i64::from(
414                values.get(index).copied().ok_or_else(|| {
415                    VmError::HostError(format!("bytes index {index} out of bounds"))
416                })?,
417            )))
418        }
419        Value::String(text) => {
420            builtin_get_string_impl(text.as_str(), key.as_int()?).map(return_one)
421        }
422        _ => Err(VmError::TypeMismatch("bytes/array/map/string")),
423    }
424}
425
426/// Return the runtime type name of a value.
427#[pd_host_function(name = "type")]
428pub(super) fn builtin_type_of_impl(value: VmValueRef<'_>) -> String {
429    match value {
430        Value::Null => "null",
431        Value::Int(_) => "int",
432        Value::Float(_) => "float",
433        Value::Bool(_) => "bool",
434        Value::String(_) => "string",
435        Value::Bytes(_) => "bytes",
436        Value::Array(_) => "array",
437        Value::Map(_) => "map",
438        Value::Callable(_) => "callable",
439    }
440    .to_string()
441}
442
443/// Convert a value into a display string.
444#[pd_host_function(name = "__to_string")]
445pub(crate) fn builtin_to_string_impl(value: VmValueRef<'_>) -> String {
446    render_value_for_display(value)
447}
448
449/// Render a format template against an array of positional values.
450#[pd_host_function(name = "__format_template")]
451pub(super) fn builtin_format_template_impl(
452    template: &str,
453    values: VmArrayRef<'_>,
454) -> VmResult<String> {
455    ParsedFormat::parse(template, values, &NoNamedArguments)
456        .map(|parsed| parsed.to_string())
457        .map_err(|offset| {
458            VmError::HostError(format!(
459                "format string and arguments are incompatible at byte {offset}: {template}"
460            ))
461        })
462}
463
464fn render_value_for_display(value: &Value) -> String {
465    match value {
466        Value::Null => "null".to_string(),
467        Value::Int(v) => v.to_string(),
468        Value::Float(v) => v.to_string(),
469        Value::Bool(v) => v.to_string(),
470        Value::String(v) => v.as_str().to_string(),
471        Value::Bytes(v) => render_bytes_for_display(v.as_ref()),
472        Value::Array(values) => {
473            let parts = values
474                .iter()
475                .map(render_value_for_display)
476                .collect::<Vec<_>>()
477                .join(", ");
478            format!("[{parts}]")
479        }
480        Value::Map(entries) => {
481            let parts = entries
482                .iter()
483                .map(|(key, value)| {
484                    format!(
485                        "{}: {}",
486                        render_value_for_display(key),
487                        render_value_for_display(value)
488                    )
489                })
490                .collect::<Vec<_>>()
491                .join(", ");
492            format!("{{{parts}}}")
493        }
494        Value::Callable(callable) => match callable.kind {
495            crate::CallableKind::FunctionItem => format!("<fn#{}>", callable.prototype_id),
496            crate::CallableKind::Closure => format!("<closure#{}>", callable.prototype_id),
497            crate::CallableKind::HostFunction => format!("<host-fn#{}>", callable.prototype_id),
498        },
499    }
500}
501
502fn render_bytes_for_display(bytes: &[u8]) -> String {
503    let preview_len = bytes.len().min(16);
504    let mut preview = String::with_capacity(preview_len * 2);
505    for byte in &bytes[..preview_len] {
506        preview.push(hex_nibble(byte >> 4));
507        preview.push(hex_nibble(byte & 0x0F));
508    }
509    if bytes.len() > preview_len {
510        format!("bytes[len={} hex={}..]", bytes.len(), preview)
511    } else {
512        format!("bytes[len={} hex={}]", bytes.len(), preview)
513    }
514}
515
516fn hex_nibble(value: u8) -> char {
517    match value {
518        0..=9 => char::from(b'0' + value),
519        10..=15 => char::from(b'a' + (value - 10)),
520        _ => unreachable!("hex nibble out of range"),
521    }
522}
523
524impl FormatArgument for Value {
525    fn supports_format(&self, specifier: &Specifier) -> bool {
526        match self {
527            Value::Null => matches!(specifier.format, Format::Display | Format::Debug),
528            Value::Int(_) => true,
529            Value::Float(_) => matches!(
530                specifier.format,
531                Format::Display | Format::Debug | Format::LowerExp | Format::UpperExp
532            ),
533            Value::Bool(_) => matches!(specifier.format, Format::Display | Format::Debug),
534            Value::String(_) | Value::Bytes(_) => {
535                matches!(specifier.format, Format::Display | Format::Debug)
536            }
537            Value::Array(_) | Value::Map(_) | Value::Callable(_) => {
538                matches!(specifier.format, Format::Display | Format::Debug)
539            }
540        }
541    }
542
543    fn fmt_display(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
544        match self {
545            Value::Null => f.write_str("null"),
546            Value::Int(value) => std::fmt::Display::fmt(value, f),
547            Value::Float(value) => std::fmt::Display::fmt(value, f),
548            Value::Bool(value) => std::fmt::Display::fmt(value, f),
549            Value::String(value) => std::fmt::Display::fmt(value.as_str(), f),
550            Value::Bytes(_) | Value::Array(_) | Value::Map(_) | Value::Callable(_) => {
551                f.write_str(render_value_for_display(self).as_str())
552            }
553        }
554    }
555
556    fn fmt_debug(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
557        match self {
558            Value::Null => f.write_str("null"),
559            Value::Int(value) => std::fmt::Debug::fmt(value, f),
560            Value::Float(value) => std::fmt::Debug::fmt(value, f),
561            Value::Bool(value) => std::fmt::Debug::fmt(value, f),
562            Value::String(value) => std::fmt::Debug::fmt(value.as_str(), f),
563            Value::Bytes(_) => f.write_str(render_value_for_display(self).as_str()),
564            Value::Array(values) => {
565                let mut list = f.debug_list();
566                for value in values.iter() {
567                    list.entry(value);
568                }
569                list.finish()
570            }
571            Value::Map(entries) => {
572                let mut map = f.debug_map();
573                for (key, value) in entries.iter() {
574                    map.entry(key, value);
575                }
576                map.finish()
577            }
578            Value::Callable(_) => f.write_str(render_value_for_display(self).as_str()),
579        }
580    }
581
582    fn fmt_octal(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
583        match self {
584            Value::Int(value) => std::fmt::Octal::fmt(value, f),
585            _ => Err(std::fmt::Error),
586        }
587    }
588
589    fn fmt_lower_hex(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
590        match self {
591            Value::Int(value) => std::fmt::LowerHex::fmt(value, f),
592            _ => Err(std::fmt::Error),
593        }
594    }
595
596    fn fmt_upper_hex(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
597        match self {
598            Value::Int(value) => std::fmt::UpperHex::fmt(value, f),
599            _ => Err(std::fmt::Error),
600        }
601    }
602
603    fn fmt_binary(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
604        match self {
605            Value::Int(value) => std::fmt::Binary::fmt(value, f),
606            _ => Err(std::fmt::Error),
607        }
608    }
609
610    fn fmt_lower_exp(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
611        match self {
612            Value::Int(value) => std::fmt::LowerExp::fmt(value, f),
613            Value::Float(value) => std::fmt::LowerExp::fmt(value, f),
614            _ => Err(std::fmt::Error),
615        }
616    }
617
618    fn fmt_upper_exp(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
619        match self {
620            Value::Int(value) => std::fmt::UpperExp::fmt(value, f),
621            Value::Float(value) => std::fmt::UpperExp::fmt(value, f),
622            _ => Err(std::fmt::Error),
623        }
624    }
625
626    fn to_usize(&self) -> Result<usize, ()> {
627        match self {
628            Value::Int(value) => usize::try_from(*value).map_err(|_| ()),
629            _ => Err(()),
630        }
631    }
632}
633
634/// Update an array entry and return the updated array.
635#[pd_host_function(name = "set")]
636pub(super) fn builtin_set_array_impl(
637    mut items: VmArrayHandle,
638    index: i64,
639    value: VmValueOwned,
640) -> VmResult<VmArrayHandle> {
641    let items_mut = Arc::make_mut(&mut items);
642    if index < 0 {
643        return Err(VmError::HostError(
644            "array index must be non-negative".to_string(),
645        ));
646    }
647    let index = usize::try_from(index)
648        .map_err(|_| VmError::HostError("array index overflow".to_string()))?;
649    if index < items_mut.len() {
650        items_mut[index] = value;
651    } else if index == items_mut.len() {
652        items_mut.push(value);
653    } else {
654        return Err(VmError::HostError(format!(
655            "array index {index} out of bounds"
656        )));
657    }
658    Ok(items)
659}
660
661pub(crate) fn builtin_set_array_shared_impl(
662    mut items: SharedArray,
663    index: i64,
664    value: AnyValue,
665) -> VmResult<SharedArray> {
666    let items_mut = Arc::make_mut(&mut items);
667    if index < 0 {
668        return Err(VmError::HostError(
669            "array index must be non-negative".to_string(),
670        ));
671    }
672    let index = usize::try_from(index)
673        .map_err(|_| VmError::HostError("array index overflow".to_string()))?;
674    if index < items_mut.len() {
675        items_mut[index] = value;
676    } else if index == items_mut.len() {
677        items_mut.push(value);
678    } else {
679        return Err(VmError::HostError(format!(
680            "array index {index} out of bounds"
681        )));
682    }
683    Ok(items)
684}
685
686/// Update a map entry and return the updated map.
687#[pd_host_function(name = "set")]
688pub(super) fn builtin_set_map_impl(
689    mut entries: VmMapHandle,
690    key: VmValueOwned,
691    value: VmValueOwned,
692) -> VmMapHandle {
693    let entries_mut = Arc::make_mut(&mut entries);
694    if matches!(value, Value::Null) {
695        entries_mut.remove(&key);
696    } else {
697        entries_mut.insert(key, value);
698    }
699    entries
700}
701
702pub(crate) fn builtin_set_map_shared_impl(
703    mut entries: SharedMap,
704    key: AnyValue,
705    value: AnyValue,
706) -> SharedMap {
707    let entries_mut = Arc::make_mut(&mut entries);
708    if matches!(value, Value::Null) {
709        entries_mut.remove(&key);
710    } else {
711        entries_mut.insert(key, value);
712    }
713    entries
714}
715
716pub(crate) fn ensure_supported_map_key(key: &Value) -> VmResult<()> {
717    if matches!(key, Value::Callable(_)) {
718        return Err(VmError::HostError(
719            "callable values are not supported as map keys".to_string(),
720        ));
721    }
722    Ok(())
723}
724
725pub(crate) fn builtin_set_owned(container: Value, key: Value, value: Value) -> VmResult<Value> {
726    match container {
727        Value::Array(values) => {
728            builtin_set_array_shared_impl(values, key.as_int()?, value).map(Value::Array)
729        }
730        Value::Map(entries) => {
731            ensure_supported_map_key(&key)?;
732            Ok(Value::Map(builtin_set_map_shared_impl(entries, key, value)))
733        }
734        _ => Err(VmError::TypeMismatch("array/map")),
735    }
736}
737
738pub(super) fn builtin_set(args: &mut [Value]) -> VmResult<CallReturn> {
739    let container = take_arg(args, 0, "set container")?;
740    let key = take_arg(args, 1, "set key")?;
741    let value = take_arg(args, 2, "set value")?;
742    builtin_set_owned(container, key, value).map(return_one)
743}
744
745/// Return an array of container keys or indices.
746#[pd_host_function(name = "keys")]
747pub(super) fn builtin_keys_array_impl(items: VmArrayRef<'_>) -> VmArray {
748    (0..items.len())
749        .map(|index| Value::Int(index as i64))
750        .collect::<Vec<_>>()
751}
752
753/// Return an array of map keys.
754#[pd_host_function(name = "keys")]
755pub(super) fn builtin_keys_map_impl(entries: VmMapRef<'_>) -> VmArray {
756    entries
757        .iter()
758        .map(|(key, _)| key.clone())
759        .collect::<Vec<_>>()
760}
761
762pub(super) fn builtin_keys(args: &[Value]) -> VmResult<CallReturn> {
763    let container = arg::<&Value>(args, 0, "keys container")?;
764    match container {
765        Value::Array(values) => Ok(return_one(
766            (0..values.len())
767                .map(|index| Value::Int(index as i64))
768                .collect::<Vec<_>>(),
769        )),
770        Value::Map(entries) => Ok(return_one(
771            entries
772                .iter()
773                .map(|(key, _)| key.clone())
774                .collect::<Vec<_>>(),
775        )),
776        _ => Err(VmError::TypeMismatch("array/map")),
777    }
778}
779
780/// Return the number of entries in an array or map.
781#[pd_host_function(name = "count")]
782pub(super) fn builtin_count_array_impl(items: VmArrayRef<'_>) -> i64 {
783    items.len() as i64
784}
785
786/// Return the number of entries in a map.
787#[pd_host_function(name = "count")]
788pub(super) fn builtin_count_map_impl(entries: VmMapRef<'_>) -> i64 {
789    entries.len() as i64
790}
791
792pub(super) fn builtin_count(args: &[Value]) -> VmResult<CallReturn> {
793    let container = arg::<&Value>(args, 0, "count container")?;
794    match container {
795        Value::Array(values) => Ok(return_one(values.len())),
796        Value::Map(entries) => Ok(return_one(entries.len())),
797        _ => Err(VmError::TypeMismatch("array/map")),
798    }
799}
800
801/// Abort execution if the condition is false.
802#[pd_host_function(name = "assert")]
803pub(super) fn builtin_assert_impl(condition: bool) -> VmResult<()> {
804    if condition {
805        Ok(())
806    } else {
807        Err(VmError::HostError("assertion failed".to_string()))
808    }
809}
810
811/// Return true when `needle` is found within `text`.
812#[pd_host_function(name = "string_contains")]
813pub(crate) fn builtin_string_contains_impl(text: VmStringRef<'_>, needle: VmStringRef<'_>) -> bool {
814    text.contains(needle)
815}
816
817/// Replace non-overlapping literal `needle` matches in `text`.
818#[pd_host_function(name = "string_replace_literal")]
819pub(crate) fn builtin_string_replace_literal_impl(
820    text: VmStringRef<'_>,
821    needle: VmStringRef<'_>,
822    replacement: VmStringRef<'_>,
823) -> String {
824    if needle.is_empty() {
825        return text.to_string();
826    }
827    text.replace(needle, replacement)
828}
829
830/// Lower ASCII `A`-`Z` bytes in `text` while preserving UTF-8.
831#[pd_host_function(name = "string_lower_ascii")]
832pub(crate) fn builtin_string_lower_ascii_impl(text: VmStringRef<'_>) -> String {
833    let mut out = text.as_bytes().to_vec();
834    for byte in &mut out {
835        if byte.is_ascii_uppercase() {
836            *byte = byte.to_ascii_lowercase();
837        }
838    }
839    String::from_utf8(out).expect("ASCII-only byte changes preserve UTF-8")
840}
841
842/// Split `text` on non-overlapping literal `delimiter` matches.
843#[pd_host_function(name = "string_split_literal")]
844pub(crate) fn builtin_string_split_literal_impl(
845    text: VmStringRef<'_>,
846    delimiter: VmStringRef<'_>,
847) -> VmArray {
848    if delimiter.is_empty() {
849        return vec![Value::string(text.to_string())];
850    }
851    text.split(delimiter)
852        .map(|part| Value::string(part.to_string()))
853        .collect()
854}
855
856/// Initialize the hidden iterator used by borrowed map for-in loops.
857#[allow(dead_code)]
858#[pd_host_function(name = "__map_iter_init")]
859fn builtin_map_iter_init_metadata(map: VmMapRef<'_>, _slot: i64) -> VmMapHandle {
860    Arc::new(map.clone())
861}
862
863/// Advance a hidden borrowed-map iterator.
864#[allow(dead_code)]
865#[pd_host_function(name = "__map_iter_next")]
866fn builtin_map_iter_next_metadata(_slot: i64) -> bool {
867    false
868}
869
870/// Move the current iterator key into the loop binding.
871#[allow(dead_code)]
872#[pd_host_function(name = "__map_iter_take_key")]
873fn builtin_map_iter_take_key_metadata(_slot: i64) -> Value {
874    Value::Null
875}
876
877/// Move the current iterator value into the loop binding.
878#[allow(dead_code)]
879#[pd_host_function(name = "__map_iter_take_value")]
880fn builtin_map_iter_take_value_metadata(_slot: i64) -> Value {
881    Value::Null
882}
883
884/// Release a hidden borrowed-map iterator after loop exit.
885#[allow(dead_code)]
886#[pd_host_function(name = "__map_iter_close")]
887fn builtin_map_iter_close_metadata(map: VmMapRef<'_>, _slot: i64) -> VmMapHandle {
888    Arc::new(map.clone())
889}
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894    use crate::builtins::{BUILTIN_CALL_BASE, BUILTIN_CALL_COUNT, BuiltinFunction};
895    use std::sync::Arc;
896
897    #[test]
898    fn internal_builtins_have_unique_reserved_call_indices() {
899        let reserved = [
900            (BuiltinFunction::MapIterInit, BUILTIN_CALL_BASE - 9),
901            (BuiltinFunction::MapIterNext, BUILTIN_CALL_BASE - 10),
902            (BuiltinFunction::MapIterTakeKey, BUILTIN_CALL_BASE - 11),
903            (BuiltinFunction::MapIterTakeValue, BUILTIN_CALL_BASE - 12),
904            (BuiltinFunction::MapIterClose, BUILTIN_CALL_BASE - 13),
905            (BuiltinFunction::BindCallable, BUILTIN_CALL_BASE - 14),
906            (BuiltinFunction::DetachLocal, BUILTIN_CALL_BASE - 15),
907        ];
908        for (builtin, index) in reserved {
909            assert_eq!(builtin.call_index(), index);
910            assert_eq!(BuiltinFunction::from_call_index(index), Some(builtin));
911        }
912        assert_eq!(BUILTIN_CALL_COUNT, 89);
913        for name in ["__bind_callable", "__detach_local"] {
914            assert!(
915                !crate::builtins::language_builtin_specs()
916                    .iter()
917                    .any(|spec| spec.name == name),
918                "internal callable metadata operation must not be language-visible: {name}"
919            );
920        }
921        for alias in BUILTIN_CALL_BASE + 89..=BUILTIN_CALL_BASE + 92 {
922            assert_eq!(BuiltinFunction::from_call_index(alias), None);
923        }
924    }
925
926    #[test]
927    fn callable_map_keys_are_rejected() {
928        let callable = Value::Callable(Arc::new(crate::CallableValue {
929            prototype_id: 0,
930            kind: crate::CallableKind::FunctionItem,
931            env: None,
932        }));
933        let err = builtin_set_owned(Value::map(Vec::new()), callable, Value::Int(1))
934            .expect_err("callable map key should fail");
935        assert!(err.to_string().contains("not supported as map keys"));
936    }
937
938    #[test]
939    fn array_push_detaches_shared_array_before_write() {
940        let shared = Value::array(vec![Value::Int(1)]);
941        let alias = shared.clone();
942
943        let mut args = [shared, Value::Int(2)];
944        let out = builtin_array_push(&mut args).expect("array push should work");
945        let [Value::Array(result)] = out.as_slice() else {
946            panic!("expected array result");
947        };
948        let Value::Array(alias_values) = &alias else {
949            panic!("expected array alias");
950        };
951
952        assert_eq!(alias_values.as_ref(), &vec![Value::Int(1)]);
953        assert_eq!(result.as_ref(), &vec![Value::Int(1), Value::Int(2)]);
954        assert!(
955            !Arc::ptr_eq(alias_values, result),
956            "mutating a shared array should detach backing storage"
957        );
958    }
959
960    #[test]
961    fn set_detaches_shared_array_before_write() {
962        let shared = Value::array(vec![Value::Int(1), Value::Int(2)]);
963        let alias = shared.clone();
964
965        let mut args = [shared, Value::Int(0), Value::Int(9)];
966        let out = builtin_set(&mut args).expect("array set should work");
967        let [Value::Array(result)] = out.as_slice() else {
968            panic!("expected array result");
969        };
970        let Value::Array(alias_values) = &alias else {
971            panic!("expected array alias");
972        };
973
974        assert_eq!(alias_values.as_ref(), &vec![Value::Int(1), Value::Int(2)]);
975        assert_eq!(result.as_ref(), &vec![Value::Int(9), Value::Int(2)]);
976        assert!(
977            !Arc::ptr_eq(alias_values, result),
978            "mutating a shared array should detach backing storage"
979        );
980    }
981
982    #[test]
983    fn set_detaches_shared_map_before_write() {
984        let shared = Value::map(vec![(Value::string("k"), Value::Int(1))]);
985        let alias = shared.clone();
986
987        let mut args = [shared, Value::string("k"), Value::Int(9)];
988        let out = builtin_set(&mut args).expect("map set should work");
989        let [Value::Map(result)] = out.as_slice() else {
990            panic!("expected map result");
991        };
992        let Value::Map(alias_entries) = &alias else {
993            panic!("expected map alias");
994        };
995
996        assert_eq!(alias_entries.len(), 1);
997        assert_eq!(alias_entries.get(&Value::string("k")), Some(&Value::Int(1)));
998        assert_eq!(result.len(), 1);
999        assert_eq!(result.get(&Value::string("k")), Some(&Value::Int(9)));
1000        assert!(
1001            !Arc::ptr_eq(alias_entries, result),
1002            "mutating a shared map should detach backing storage"
1003        );
1004    }
1005
1006    #[test]
1007    fn set_map_null_removes_entry() {
1008        let shared = Value::map(vec![(Value::string("drop"), Value::Int(1))]);
1009        let alias = shared.clone();
1010
1011        let mut args = [shared, Value::string("drop"), Value::Null];
1012        let out = builtin_set(&mut args).expect("map null set should work");
1013        let [Value::Map(result)] = out.as_slice() else {
1014            panic!("expected map result");
1015        };
1016        let Value::Map(alias_entries) = &alias else {
1017            panic!("expected map alias");
1018        };
1019
1020        assert_eq!(alias_entries.len(), 1);
1021        assert_eq!(
1022            alias_entries.get(&Value::string("drop")),
1023            Some(&Value::Int(1))
1024        );
1025        assert_eq!(result.len(), 0);
1026        assert_eq!(result.get(&Value::string("drop")), None);
1027        assert!(
1028            !Arc::ptr_eq(alias_entries, result),
1029            "mutating a shared map should detach backing storage"
1030        );
1031    }
1032
1033    #[test]
1034    fn has_map_uses_identity_for_heap_keys() {
1035        let key = Value::array(vec![Value::Int(1), Value::Int(2)]);
1036        let alias = key.clone();
1037        let structural_peer = Value::array(vec![Value::Int(1), Value::Int(2)]);
1038        let map = VmMap::from(vec![(key, Value::Bool(true))]);
1039
1040        assert!(builtin_has_map_impl(&map, &alias));
1041        assert!(!builtin_has_map_impl(&map, &structural_peer));
1042    }
1043
1044    #[test]
1045    fn has_dispatch_uses_identity_for_heap_keys() {
1046        let key = Value::array(vec![Value::Int(1), Value::Int(2)]);
1047        let alias = key.clone();
1048        let structural_peer = Value::array(vec![Value::Int(1), Value::Int(2)]);
1049        let map = Value::map(vec![(key, Value::Bool(true))]);
1050
1051        let alias_result = builtin_has(&[map.clone(), alias]).expect("builtin has should succeed");
1052        let [Value::Bool(alias_present)] = alias_result.as_slice() else {
1053            panic!("expected bool result");
1054        };
1055        assert!(*alias_present, "shared heap key should match by identity");
1056
1057        let peer_result = builtin_has(&[map, structural_peer]).expect("builtin has should succeed");
1058        let [Value::Bool(peer_present)] = peer_result.as_slice() else {
1059            panic!("expected bool result");
1060        };
1061        assert!(
1062            !peer_present,
1063            "structural peer should not match a map key stored by heap identity"
1064        );
1065    }
1066
1067    #[test]
1068    fn len_and_count_dispatch_return_shared_container_sizes() {
1069        let array = Value::array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
1070        let map = Value::map(vec![
1071            (Value::string("a"), Value::Int(1)),
1072            (Value::string("b"), Value::Int(2)),
1073        ]);
1074
1075        let array_len =
1076            builtin_len(std::slice::from_ref(&array)).expect("array len should succeed");
1077        let [Value::Int(array_len)] = array_len.as_slice() else {
1078            panic!("expected int result");
1079        };
1080        assert_eq!(*array_len, 3);
1081
1082        let array_count = builtin_count(&[array]).expect("array count should succeed");
1083        let [Value::Int(array_count)] = array_count.as_slice() else {
1084            panic!("expected int result");
1085        };
1086        assert_eq!(*array_count, 3);
1087
1088        let map_len = builtin_len(std::slice::from_ref(&map)).expect("map len should succeed");
1089        let [Value::Int(map_len)] = map_len.as_slice() else {
1090            panic!("expected int result");
1091        };
1092        assert_eq!(*map_len, 2);
1093
1094        let map_count = builtin_count(&[map]).expect("map count should succeed");
1095        let [Value::Int(map_count)] = map_count.as_slice() else {
1096            panic!("expected int result");
1097        };
1098        assert_eq!(*map_count, 2);
1099    }
1100}