Skip to main content

hara_native/core/
primitive.rs

1/// Value-level primitive operations shared by the tree-walking evaluator and
2/// the experimental bytecode VM (issue #195, notes/rust-bytecode-vm.md).
3/// All arithmetic and comparison semantics live here exactly once.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum IntrinsicOp {
6    Add,
7    Subtract,
8    Multiply,
9    Divide,
10    Remainder,
11    Modulo,
12    Equal,
13    Less,
14    LessOrEqual,
15    Greater,
16    GreaterOrEqual,
17}
18
19impl IntrinsicOp {
20    #[cfg(test)]
21    pub(crate) const ALL: &[IntrinsicOp] = &[
22        IntrinsicOp::Add,
23        IntrinsicOp::Subtract,
24        IntrinsicOp::Multiply,
25        IntrinsicOp::Divide,
26        IntrinsicOp::Remainder,
27        IntrinsicOp::Modulo,
28        IntrinsicOp::Equal,
29        IntrinsicOp::Less,
30        IntrinsicOp::LessOrEqual,
31        IntrinsicOp::Greater,
32        IntrinsicOp::GreaterOrEqual,
33    ];
34
35    pub fn from_symbol(symbol: &str) -> Option<IntrinsicOp> {
36        Some(match symbol {
37            "+" => IntrinsicOp::Add,
38            "-" => IntrinsicOp::Subtract,
39            "*" => IntrinsicOp::Multiply,
40            "/" => IntrinsicOp::Divide,
41            "mod" => IntrinsicOp::Modulo,
42            "=" => IntrinsicOp::Equal,
43            "<" => IntrinsicOp::Less,
44            "<=" => IntrinsicOp::LessOrEqual,
45            ">" => IntrinsicOp::Greater,
46            ">=" => IntrinsicOp::GreaterOrEqual,
47            _ => return None,
48        })
49    }
50
51    /// Canonical operator spelling used in errors and compiler diagnostics.
52    pub fn operator(self) -> &'static str {
53        match self {
54            IntrinsicOp::Add => "+",
55            IntrinsicOp::Subtract => "-",
56            IntrinsicOp::Multiply => "*",
57            IntrinsicOp::Divide => "/",
58            IntrinsicOp::Remainder => "rem",
59            IntrinsicOp::Modulo => "mod",
60            IntrinsicOp::Equal => "=",
61            IntrinsicOp::Less => "<",
62            IntrinsicOp::LessOrEqual => "<=",
63            IntrinsicOp::Greater => ">",
64            IntrinsicOp::GreaterOrEqual => ">=",
65        }
66    }
67}
68
69/// Applies a primitive to already-evaluated arguments. The evaluator calls
70/// this after evaluating argument forms; the bytecode VM calls it directly
71/// from the operand stack.
72pub(crate) fn apply_intrinsic(
73    primitive: IntrinsicOp,
74    arguments: &[Value],
75) -> Result<Value, String> {
76    let op = primitive.operator();
77    if let [left, right] = arguments {
78        return apply_binary_intrinsic(primitive, left, right);
79    }
80    match primitive {
81        IntrinsicOp::Add
82        | IntrinsicOp::Subtract
83        | IntrinsicOp::Multiply
84        | IntrinsicOp::Divide
85        | IntrinsicOp::Remainder
86        | IntrinsicOp::Modulo => {
87            if arguments.is_empty() {
88                return Err(format!("{op} expects arguments"));
89            }
90            if matches!(primitive, IntrinsicOp::Remainder | IntrinsicOp::Modulo)
91                && arguments.len() != 2
92            {
93                return Err(format!("{op} expects two numbers"));
94            }
95            if arguments.len() == 1 {
96                if primitive == IntrinsicOp::Subtract {
97                    return numeric::numeric_negate(&arguments[0]);
98                }
99                if primitive == IntrinsicOp::Divide {
100                    return apply_binary_intrinsic(
101                        IntrinsicOp::Divide,
102                        &Value::Number(1),
103                        &arguments[0],
104                    );
105                }
106                if !numeric::is_numeric_value(&arguments[0]) {
107                    return Err(format!("{op} expects numbers"));
108                }
109                return Ok(arguments[0].clone());
110            }
111            let mut result = arguments[0].clone();
112            for argument in &arguments[1..] {
113                result = apply_binary_intrinsic(primitive, &result, argument)?;
114            }
115            Ok(result)
116        }
117        IntrinsicOp::Equal => {
118            if arguments.len() < 2 {
119                return Err("= expects at least 2 arguments".into());
120            }
121            let first = &arguments[0];
122            Ok(Value::Bool(
123                arguments[1..].iter().all(|value| value == first),
124            ))
125        }
126        IntrinsicOp::Less
127        | IntrinsicOp::LessOrEqual
128        | IntrinsicOp::Greater
129        | IntrinsicOp::GreaterOrEqual => {
130            if arguments.len() < 2 {
131                return Err(format!("{op} expects at least two arguments"));
132            }
133            for pair in arguments.windows(2) {
134                let Some(ordering) = numeric::numeric_compare(&pair[0], &pair[1])? else {
135                    return Err(format!("{op} expects numbers"));
136                };
137                let matches = match primitive {
138                    IntrinsicOp::Less => ordering == std::cmp::Ordering::Less,
139                    IntrinsicOp::LessOrEqual => ordering != std::cmp::Ordering::Greater,
140                    IntrinsicOp::Greater => ordering == std::cmp::Ordering::Greater,
141                    IntrinsicOp::GreaterOrEqual => ordering != std::cmp::Ordering::Less,
142                    _ => unreachable!(),
143                };
144                if !matches {
145                    return Ok(Value::Bool(false));
146                }
147            }
148            Ok(Value::Bool(true))
149        }
150    }
151}
152
153pub(crate) fn apply_intrinsic_name(name: &str, arguments: &[Value]) -> Result<Value, String> {
154    if let Some(primitive) = IntrinsicOp::from_symbol(name) {
155        return apply_intrinsic(primitive, arguments);
156    }
157    let native = name
158        .strip_prefix("std.native.")
159        .ok_or_else(|| format!("unknown runtime intrinsic: {name}"))?;
160    let (native_type, method) = native
161        .split_once('/')
162        .ok_or_else(|| format!("invalid native intrinsic target: {name}"))?;
163    let callable = native_type_function_value(native_type, method)?;
164    call_value(callable, arguments.to_vec())
165}
166
167/// Applies the common fixed-arity primitive case without constructing an
168/// argument slice. The bytecode VM uses this directly on its operand stack;
169/// the general evaluator reaches the same helper through [`apply_intrinsic`].
170pub(crate) fn apply_binary_intrinsic(
171    primitive: IntrinsicOp,
172    left: &Value,
173    right: &Value,
174) -> Result<Value, String> {
175    let op = primitive.operator();
176    if let (Value::Number(left), Value::Number(right)) = (left, right) {
177        return apply_binary_numbers(primitive, *left, *right);
178    }
179    match primitive {
180        IntrinsicOp::Equal => return Ok(Value::Bool(left == right)),
181        IntrinsicOp::Less
182        | IntrinsicOp::LessOrEqual
183        | IntrinsicOp::Greater
184        | IntrinsicOp::GreaterOrEqual => {
185            let Some(ordering) = numeric::numeric_compare(left, right)? else {
186                return Err(format!("{op} expects numbers"));
187            };
188            return Ok(Value::Bool(match primitive {
189                IntrinsicOp::Less => ordering == std::cmp::Ordering::Less,
190                IntrinsicOp::LessOrEqual => ordering != std::cmp::Ordering::Greater,
191                IntrinsicOp::Greater => ordering == std::cmp::Ordering::Greater,
192                IntrinsicOp::GreaterOrEqual => ordering != std::cmp::Ordering::Less,
193                _ => unreachable!(),
194            }));
195        }
196        IntrinsicOp::Add
197        | IntrinsicOp::Subtract
198        | IntrinsicOp::Multiply
199        | IntrinsicOp::Divide
200        | IntrinsicOp::Remainder
201        | IntrinsicOp::Modulo => {
202            let operation = match primitive {
203                IntrinsicOp::Add => ArithmeticOp::Add,
204                IntrinsicOp::Subtract => ArithmeticOp::Subtract,
205                IntrinsicOp::Multiply => ArithmeticOp::Multiply,
206                IntrinsicOp::Divide => ArithmeticOp::Divide,
207                IntrinsicOp::Remainder => ArithmeticOp::Remainder,
208                IntrinsicOp::Modulo => ArithmeticOp::Modulo,
209                _ => unreachable!(),
210            };
211            return numeric::numeric_binary(operation, left, right).map_err(|error| {
212                if error == "expected numeric values" {
213                    format!("{op} expects numbers")
214                } else {
215                    error
216                }
217            });
218        }
219    }
220}
221
222pub(crate) fn apply_binary_numbers(
223    primitive: IntrinsicOp,
224    left: i64,
225    right: i64,
226) -> Result<Value, String> {
227    apply_binary_numbers_promoting(primitive, left, right)
228}
229
230fn apply_binary_numbers_promoting(
231    primitive: IntrinsicOp,
232    left: i64,
233    right: i64,
234) -> Result<Value, String> {
235    let result = match primitive {
236        IntrinsicOp::Add => match left.checked_add(right) {
237            Some(value) => Value::Number(value),
238            None => {
239                return numeric::numeric_binary(
240                    ArithmeticOp::Add,
241                    &Value::Number(left),
242                    &Value::Number(right),
243                )
244            }
245        },
246        IntrinsicOp::Subtract => match left.checked_sub(right) {
247            Some(value) => Value::Number(value),
248            None => {
249                return numeric::numeric_binary(
250                    ArithmeticOp::Subtract,
251                    &Value::Number(left),
252                    &Value::Number(right),
253                )
254            }
255        },
256        IntrinsicOp::Multiply => match left.checked_mul(right) {
257            Some(value) => Value::Number(value),
258            None => {
259                return numeric::numeric_binary(
260                    ArithmeticOp::Multiply,
261                    &Value::Number(left),
262                    &Value::Number(right),
263                )
264            }
265        },
266        IntrinsicOp::Divide | IntrinsicOp::Remainder | IntrinsicOp::Modulo if right == 0 => {
267            return Err("division by zero".into())
268        }
269        IntrinsicOp::Divide => match left.checked_div(right) {
270            Some(value) => Value::Number(value),
271            None => {
272                return numeric::numeric_binary(
273                    ArithmeticOp::Divide,
274                    &Value::Number(left),
275                    &Value::Number(right),
276                )
277            }
278        },
279        IntrinsicOp::Remainder | IntrinsicOp::Modulo => {
280            if left == i64::MIN && right == -1 {
281                Value::Number(0)
282            } else {
283                Value::Number(
284                    left.checked_rem(right)
285                        .expect("remainder overflow handled above"),
286                )
287            }
288        }
289        IntrinsicOp::Equal => Value::Bool(left == right),
290        IntrinsicOp::Less
291        | IntrinsicOp::LessOrEqual
292        | IntrinsicOp::Greater
293        | IntrinsicOp::GreaterOrEqual => {
294            let ordering = left.cmp(&right);
295            Value::Bool(match primitive {
296                IntrinsicOp::Less => ordering == std::cmp::Ordering::Less,
297                IntrinsicOp::LessOrEqual => ordering != std::cmp::Ordering::Greater,
298                IntrinsicOp::Greater => ordering == std::cmp::Ordering::Greater,
299                IntrinsicOp::GreaterOrEqual => ordering != std::cmp::Ordering::Less,
300                _ => unreachable!(),
301            })
302        }
303    };
304    Ok(result)
305}
306
307#[cfg(test)]
308mod primitive_tests {
309    use super::{apply_binary_intrinsic, apply_intrinsic_name, IntrinsicOp};
310    use crate::core::Value;
311
312    #[test]
313    fn compiler_aliases_keep_modulo_named_and_percent_unbound() {
314        assert_eq!(IntrinsicOp::from_symbol("%"), None);
315        assert_eq!(IntrinsicOp::from_symbol("mod"), Some(IntrinsicOp::Modulo));
316        assert_eq!(IntrinsicOp::from_symbol("+"), Some(IntrinsicOp::Add));
317        assert_eq!(IntrinsicOp::from_symbol("-"), Some(IntrinsicOp::Subtract));
318    }
319
320    #[test]
321    fn mod_and_remainder_keep_the_dividend_sign() {
322        assert_eq!(
323            apply_intrinsic_name("mod", &[Value::Number(-7), Value::Number(3)]).unwrap(),
324            Value::Number(-1)
325        );
326        assert_eq!(
327            apply_intrinsic_name("mod", &[Value::Number(7), Value::Number(-3)]).unwrap(),
328            Value::Number(1)
329        );
330        assert_eq!(
331            apply_binary_intrinsic(
332                IntrinsicOp::Remainder,
333                &Value::Number(-7),
334                &Value::Number(3),
335            )
336            .unwrap(),
337            Value::Number(-1)
338        );
339    }
340}
341
342fn bit_values(op: &str, values: &[Value]) -> Result<Value, String> {
343    let op = match op.strip_prefix("std.native.Bits/").unwrap_or(op) {
344        "and" => "bit-and",
345        "or" => "bit-or",
346        "xor" => "bit-xor",
347        "not" => "bit-not",
348        "shift-left" => "bit-shift-left",
349        "shift-right" => "bit-shift-right",
350        operation => operation,
351    };
352    match op {
353        "bit-not" => {
354            if values.len() != 1 {
355                return Err("bit-not expects one integer".into());
356            }
357            numeric::bit_not(&values[0]).map_err(|_| "bit-not expects one integer".to_string())
358        }
359        "bit-and" | "bit-or" | "bit-xor" => {
360            if values.len() != 2 {
361                return Err(format!("{op} expects two integers"));
362            }
363            numeric::bit_binary(op, &values[0], &values[1]).map_err(|error| {
364                if error == "expected an integer" {
365                    format!("{op} expects integers")
366                } else {
367                    error
368                }
369            })
370        }
371        "bit-shift-left" | "bit-shift-right" => {
372            if values.len() != 2 {
373                return Err(format!("{op} expects an integer and distance"));
374            }
375            numeric::bit_shift(op == "bit-shift-left", &values[0], &values[1])
376        }
377        _ => Err(format!("unknown bit operation: {op}")),
378    }
379}
380
381pub(crate) fn number_conversion_value(operation: &str, value: Value) -> Result<Value, String> {
382    let operation = operation
383        .strip_prefix("std.native.Num/")
384        .unwrap_or(operation);
385    match operation {
386        "long" => Ok(Value::Number(
387            numeric::to_i64_truncating(&value).map_err(|error| format!("long: {error}"))?,
388        )),
389        "double" => Ok(Value::Float(
390            numeric::to_f64_explicit(&value).map_err(|error| format!("double: {error}"))?,
391        )),
392        "parse-long" => match value {
393            Value::String(value) if !value.is_empty() && value.trim() == value => Ok(value
394                .parse::<i64>()
395                .map(Value::Number)
396                .unwrap_or(Value::Nil)),
397            Value::String(_) => Ok(Value::Nil),
398            _ => Err("parse-long expects a string".into()),
399        },
400        "parse-double" => match value {
401            Value::String(value) if !value.is_empty() && value.trim() == value => {
402                if matches!(
403                    value.as_str(),
404                    "NaN" | "Infinity" | "+Infinity" | "-Infinity"
405                ) {
406                    return Err("non-finite number".into());
407                }
408                if !decimal_double_text(&value) {
409                    return Ok(Value::Nil);
410                }
411                let parsed = value.parse::<f64>().map_err(|_| "non-finite number")?;
412                Ok(Value::Float(numeric::finite_float(parsed)?))
413            }
414            Value::String(_) => Ok(Value::Nil),
415            _ => Err("parse-double expects a string".into()),
416        },
417        _ => Err(format!("unknown number conversion: {operation}")),
418    }
419}
420
421fn decimal_double_text(value: &str) -> bool {
422    let bytes = value.as_bytes();
423    let mut index = usize::from(matches!(bytes.first(), Some(b'+') | Some(b'-')));
424    let mut digits = 0usize;
425    while matches!(bytes.get(index), Some(b'0'..=b'9')) {
426        digits += 1;
427        index += 1;
428    }
429    if bytes.get(index) == Some(&b'.') {
430        index += 1;
431        while matches!(bytes.get(index), Some(b'0'..=b'9')) {
432            digits += 1;
433            index += 1;
434        }
435    }
436    if digits == 0 {
437        return false;
438    }
439    if matches!(bytes.get(index), Some(b'e') | Some(b'E')) {
440        index += 1;
441        if matches!(bytes.get(index), Some(b'+') | Some(b'-')) {
442            index += 1;
443        }
444        let exponent_start = index;
445        while matches!(bytes.get(index), Some(b'0'..=b'9')) {
446            index += 1;
447        }
448        if index == exponent_start {
449            return false;
450        }
451    }
452    index == bytes.len()
453}
454
455fn numeric_to_f64(value: &Value, operation: &str) -> Result<f64, String> {
456    numeric::to_f64_explicit(value).map_err(|error| format!("{operation}: {error}"))
457}
458
459fn numeric_abs(value: Value) -> Result<Value, String> {
460    numeric::numeric_abs(&value).map_err(|_| "abs expects a numeric value".to_string())
461}
462
463fn math_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
464    let operation = operation
465        .strip_prefix("std.native.Maths/")
466        .unwrap_or(operation);
467    let expected = if matches!(operation, "atan2" | "pow") {
468        2
469    } else {
470        1
471    };
472    if values.len() != expected {
473        return Err(format!(
474            "{operation} expects {} numeric {}",
475            if expected == 1 { "one" } else { "two" },
476            if expected == 1 { "value" } else { "values" }
477        ));
478    }
479    if operation == "abs" {
480        return numeric_abs(values.into_iter().next().unwrap());
481    }
482    let first = numeric_to_f64(&values[0], operation)?;
483    let result = match operation {
484        "acos" => first.acos(),
485        "acosh" => first.acosh(),
486        "asin" => first.asin(),
487        "asinh" => first.asinh(),
488        "atan" => first.atan(),
489        "atan2" => first.atan2(numeric_to_f64(&values[1], operation)?),
490        "atanh" => first.atanh(),
491        "ceil" => first.ceil(),
492        "cos" => first.cos(),
493        "cosh" => first.cosh(),
494        "exp" => first.exp(),
495        "floor" => first.floor(),
496        "pow" => first.powf(numeric_to_f64(&values[1], operation)?),
497        "sin" => first.sin(),
498        "sinh" => first.sinh(),
499        "sqrt" => first.sqrt(),
500        "tan" => first.tan(),
501        "tanh" => first.tanh(),
502        _ => return Err(format!("unknown math operation: {operation}")),
503    };
504    Ok(Value::Float(numeric::finite_float(result)?))
505}
506
507#[derive(Clone, Debug)]
508enum DocumentOp {
509    Text(String, usize),
510    Pass(String),
511    Escaped(String),
512    Line(String, String),
513    Break,
514    Begin(usize),
515    End,
516    Nest(i64),
517    Align(i64),
518    Outdent,
519}
520
521#[derive(Clone)]
522enum DocumentTask {
523    Visit(Value),
524    Emit(DocumentOp),
525}
526
527fn document_tag(name: &str, children: Vec<Value>) -> Result<Value, String> {
528    let mut values = Vec::with_capacity(children.len() + 1);
529    values.push(Value::Keyword(Keyword::parse(name)?));
530    values.extend(children);
531    Ok(Value::Vector(values.into()))
532}
533
534fn document_values(value: &Value) -> Option<Vec<Value>> {
535    match value {
536        Value::Vector(values) => Some(values.iter().cloned().collect()),
537        Value::Tuple(values) => Some(values.iter().cloned().collect()),
538        Value::List(values) => Some(values.iter().cloned().collect()),
539        Value::Cons(values) => Some(values.iter().collect()),
540        _ => None,
541    }
542}
543
544fn document_text(values: &[Value], operation: &str) -> Result<String, String> {
545    let mut output = String::new();
546    for value in values {
547        match value {
548            Value::String(text) => output.push_str(text),
549            Value::Character(character) => output.push(*character),
550            _ => {
551                return Err(format!(
552                    "std.native.Document/{operation} expects text values"
553                ))
554            }
555        }
556    }
557    Ok(output)
558}
559
560fn document_offset(values: &[Value], fallback: i64) -> (i64, &[Value]) {
561    match values.first() {
562        Some(Value::Number(offset)) => (*offset, &values[1..]),
563        _ => (fallback, values),
564    }
565}
566
567fn push_document_children(stack: &mut Vec<DocumentTask>, values: &[Value]) {
568    for child in values.iter().rev() {
569        stack.push(DocumentTask::Visit(child.clone()));
570    }
571}
572
573fn serialize_document(document: &Value) -> Result<Vec<DocumentOp>, String> {
574    let mut stack = vec![DocumentTask::Visit(document.clone())];
575    let mut operations = Vec::new();
576    while let Some(task) = stack.pop() {
577        match task {
578            DocumentTask::Emit(operation) => operations.push(operation),
579            DocumentTask::Visit(Value::Nil) => {}
580            DocumentTask::Visit(Value::String(text)) => {
581                let width = text.chars().count();
582                operations.push(DocumentOp::Text(text, width));
583            }
584            DocumentTask::Visit(Value::Keyword(tag))
585                if matches!(tag.as_str(), "line" | "document/line") =>
586            {
587                operations.push(DocumentOp::Line(" ".into(), "".into()));
588            }
589            DocumentTask::Visit(value) => {
590                let values = document_values(&value)
591                    .ok_or_else(|| "Document expects strings or element vectors".to_string())?;
592                if values.is_empty() {
593                    continue;
594                }
595                let tag = match &values[0] {
596                    Value::Keyword(tag) => tag.as_str(),
597                    _ => {
598                        push_document_children(&mut stack, &values);
599                        continue;
600                    }
601                };
602                let body = &values[1..];
603                match tag {
604                    "text" | "document/text" => {
605                        let text = document_text(body, "text")?;
606                        let width = text.chars().count();
607                        operations.push(DocumentOp::Text(text, width));
608                    }
609                    "pass" | "document/pass" => {
610                        operations.push(DocumentOp::Pass(document_text(body, "pass")?));
611                    }
612                    "escaped" | "document/escaped" => {
613                        if body.len() != 1 {
614                            return Err("std.native.Document/escaped expects one string".into());
615                        }
616                        operations.push(DocumentOp::Escaped(document_text(body, "escaped")?));
617                    }
618                    "span" | "document/span" | "document/fragment" => {
619                        push_document_children(&mut stack, body);
620                    }
621                    "annotate" | "document/annotate" => {
622                        if body.is_empty() {
623                            return Err("std.native.Document/annotate expects an annotation".into());
624                        }
625                        push_document_children(&mut stack, &body[1..]);
626                    }
627                    "line" | "document/line" => {
628                        if body.len() > 2 {
629                            return Err(
630                                "std.native.Document/line expects optional inline and terminate text"
631                                    .into(),
632                            );
633                        }
634                        let inline = if body.is_empty() {
635                            " ".into()
636                        } else {
637                            document_text(&body[..1], "line")?
638                        };
639                        let terminate = if body.len() < 2 {
640                            "".into()
641                        } else {
642                            document_text(&body[1..2], "line")?
643                        };
644                        operations.push(DocumentOp::Line(inline, terminate));
645                    }
646                    "break" | "document/break" => {
647                        if !body.is_empty() {
648                            return Err("std.native.Document/break expects no arguments".into());
649                        }
650                        operations.push(DocumentOp::Break);
651                    }
652                    "group" | "document/group" => {
653                        stack.push(DocumentTask::Emit(DocumentOp::End));
654                        push_document_children(&mut stack, body);
655                        stack.push(DocumentTask::Emit(DocumentOp::Begin(0)));
656                    }
657                    "nest" | "document/nest" => {
658                        let (offset, children) = document_offset(body, 2);
659                        stack.push(DocumentTask::Emit(DocumentOp::Outdent));
660                        push_document_children(&mut stack, children);
661                        stack.push(DocumentTask::Emit(DocumentOp::Nest(offset)));
662                    }
663                    "align" | "document/align" => {
664                        let (offset, children) = document_offset(body, 0);
665                        stack.push(DocumentTask::Emit(DocumentOp::Outdent));
666                        push_document_children(&mut stack, children);
667                        stack.push(DocumentTask::Emit(DocumentOp::Align(offset)));
668                    }
669                    _ => {
670                        return Err(format!(
671                            "Document text renderer does not support element tag :{tag}"
672                        ))
673                    }
674                }
675            }
676        }
677    }
678    Ok(operations)
679}
680
681fn annotate_document_groups(operations: &mut [DocumentOp]) -> Result<Vec<usize>, String> {
682    let mut right = 0usize;
683    let mut rights = Vec::with_capacity(operations.len());
684    let mut groups = Vec::new();
685    for index in 0..operations.len() {
686        let operation = operations[index].clone();
687        match operation {
688            DocumentOp::Text(_, width) => right = right.saturating_add(width),
689            DocumentOp::Escaped(_) => right = right.saturating_add(1),
690            DocumentOp::Line(inline, _) => right = right.saturating_add(inline.chars().count()),
691            DocumentOp::Begin(_) => groups.push(index),
692            DocumentOp::End => {
693                let begin = groups
694                    .pop()
695                    .ok_or_else(|| "Document contains an unmatched group end".to_string())?;
696                operations[begin] = DocumentOp::Begin(right);
697            }
698            _ => {}
699        }
700        rights.push(right);
701    }
702    if !groups.is_empty() {
703        return Err("Document contains an unmatched group begin".into());
704    }
705    Ok(rights)
706}
707
708fn render_document_text(document: &Value, width: usize) -> Result<String, String> {
709    let mut operations = serialize_document(document)?;
710    let rights = annotate_document_groups(&mut operations)?;
711    let mut output = String::new();
712    let mut fits = 0usize;
713    let mut length = width;
714    let mut tabs = vec![0i64];
715    let mut column = 0i64;
716    for (index, operation) in operations.into_iter().enumerate() {
717        let indent = *tabs.last().unwrap_or(&0);
718        match operation {
719            DocumentOp::Text(text, visible) => {
720                if column == 0 && indent > 0 {
721                    output.push_str(&" ".repeat(indent as usize));
722                    column += indent;
723                }
724                output.push_str(&text);
725                column += visible as i64;
726            }
727            DocumentOp::Escaped(text) => {
728                if column == 0 && indent > 0 {
729                    output.push_str(&" ".repeat(indent as usize));
730                    column += indent;
731                }
732                output.push_str(&text);
733                column += 1;
734            }
735            DocumentOp::Pass(text) => output.push_str(&text),
736            DocumentOp::Line(inline, terminate) => {
737                if fits == 0 {
738                    output.push_str(&terminate);
739                    output.push('\n');
740                    column = 0;
741                    length = rights[index]
742                        .saturating_add(width)
743                        .saturating_sub(indent.max(0) as usize);
744                } else {
745                    column += inline.chars().count() as i64;
746                    output.push_str(&inline);
747                }
748            }
749            DocumentOp::Break => {
750                output.push('\n');
751                column = 0;
752                length = rights[index]
753                    .saturating_add(width)
754                    .saturating_sub(indent.max(0) as usize);
755            }
756            DocumentOp::Nest(offset) => tabs.push(indent + offset),
757            DocumentOp::Align(offset) => tabs.push(column + offset),
758            DocumentOp::Outdent => {
759                if tabs.len() == 1 {
760                    return Err("Document contains an unmatched outdent".into());
761                }
762                tabs.pop();
763            }
764            DocumentOp::Begin(end) => {
765                fits = if fits > 0 {
766                    fits + 1
767                } else if end <= length {
768                    1
769                } else {
770                    0
771                };
772            }
773            DocumentOp::End => fits = fits.saturating_sub(1),
774        }
775    }
776    if tabs.len() != 1 {
777        return Err("Document contains an unmatched indentation scope".into());
778    }
779    Ok(output)
780}
781
782fn document_map_option(options: &Value, name: &str) -> Option<Value> {
783    let key = Value::Keyword(Keyword::parse(name).ok()?);
784    map_entries(options)?
785        .into_iter()
786        .find_map(|(candidate, value)| (candidate == key).then_some(value))
787}
788
789fn document_operation(operation: &str, values: Vec<Value>) -> Result<Value, String> {
790    let operation = operation
791        .strip_prefix("std.native.Document/")
792        .unwrap_or(operation);
793    match operation {
794        "element" => {
795            if values.is_empty() || !matches!(values[0], Value::Keyword(_)) {
796                return Err("std.native.Document/element expects a keyword tag".into());
797            }
798            Ok(Value::Vector(values.into()))
799        }
800        "text" => Ok(Value::String(document_text(&values, "text")?)),
801        "fragment" | "group" | "pass" => document_tag(&format!("document/{operation}"), values),
802        "annotate" => {
803            if values.is_empty() {
804                return Err("std.native.Document/annotate expects an annotation".into());
805            }
806            document_tag("document/annotate", values)
807        }
808        "escaped" => {
809            if values.len() != 1 || !matches!(values[0], Value::String(_)) {
810                return Err("std.native.Document/escaped expects one string".into());
811            }
812            document_tag("document/escaped", values)
813        }
814        "line" => {
815            if values.len() > 2
816                || values
817                    .iter()
818                    .any(|value| !matches!(value, Value::String(_)))
819            {
820                return Err(
821                    "std.native.Document/line expects optional inline and terminate strings".into(),
822                );
823            }
824            document_tag("document/line", values)
825        }
826        "break" => {
827            if !values.is_empty() {
828                return Err("std.native.Document/break expects no arguments".into());
829            }
830            document_tag("document/break", values)
831        }
832        "nest" | "align" => document_tag(&format!("document/{operation}"), values),
833        "normalize" => {
834            if values.len() != 1 {
835                return Err("std.native.Document/normalize expects one document".into());
836            }
837            serialize_document(&values[0])?;
838            Ok(values[0].clone())
839        }
840        "valid?" => {
841            if values.len() != 1 {
842                return Err("std.native.Document/valid? expects one value".into());
843            }
844            Ok(Value::Bool(serialize_document(&values[0]).is_ok()))
845        }
846        "render" => {
847            if !(1..=2).contains(&values.len()) {
848                return Err(
849                    "std.native.Document/render expects a document and optional options map".into(),
850                );
851            }
852            let default_options = Value::Map(PMap::new());
853            let options = values.get(1).unwrap_or(&default_options);
854            if map_entries(options).is_none() {
855                return Err("std.native.Document/render expects an options map".into());
856            }
857            match document_map_option(options, "format") {
858                None => {}
859                Some(Value::Keyword(value)) if value.as_str() == "text" => {}
860                _ => return Err("std.native.Document/render currently supports only :text".into()),
861            }
862            let width = match document_map_option(options, "width") {
863                None => 80usize,
864                Some(Value::Number(value)) if value >= 0 => value as usize,
865                Some(_) => {
866                    return Err(
867                        "std.native.Document/render width must be a non-negative integer".into(),
868                    )
869                }
870            };
871            Ok(Value::String(render_document_text(&values[0], width)?))
872        }
873        _ => Err(format!("unknown Document operation: {operation}")),
874    }
875}
876
877fn result_context(value: Option<Value>) -> Result<Value, String> {
878    let context = value.unwrap_or_else(|| Value::Map(PMap::new()));
879    map_entries(&context)
880        .is_some()
881        .then_some(context)
882        .ok_or_else(|| "Result context must be a map".into())
883}
884
885fn result_synchronize_options(options: Option<Value>) -> Result<(Option<u64>, Value), String> {
886    let Some(options) = options else {
887        return Ok((None, Value::Map(PMap::new())));
888    };
889    if map_entries(&options).is_none() {
890        return Err("std.native.Result/synchronize expects an options map".into());
891    }
892    let timeout_key = Value::Keyword(Keyword::from("timeout"));
893    let context_key = Value::Keyword(Keyword::from("context"));
894    let timeout = match map_value(&options, &timeout_key) {
895        None | Some(Value::Nil) => None,
896        Some(value) => Some(
897            value_u64_integer(value, "std.native.Result/synchronize").map_err(|_| {
898                "std.native.Result/synchronize timeout must be a non-negative integer".to_string()
899            })?,
900        ),
901    };
902    let context = result_context(map_value(&options, &context_key).cloned())?;
903    Ok((timeout, context))
904}
905
906fn native_result_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
907    let operation = operation
908        .strip_prefix("std.native.Result/")
909        .unwrap_or(operation);
910    match operation {
911        "create" => {
912            if !(2..=3).contains(&values.len()) {
913                return Err(
914                    "std.native.Result/create expects status, value, and optional context".into(),
915                );
916            }
917            let status = values[0].clone();
918            let value = values[1].clone();
919            let context = result_context(values.get(2).cloned())?;
920            match status {
921                Value::Keyword(status) if status.as_str() == "success" => Ok(Value::Result(
922                    Rc::new(ResultValue::success(value, context)?),
923                )),
924                Value::Keyword(status) if status.as_str() == "error" => {
925                    Ok(Value::Result(Rc::new(ResultValue::error(value, context)?)))
926                }
927                _ => Err("std.native.Result/create status must be :success or :error".into()),
928            }
929        }
930        "synchronize" => {
931            if !(1..=2).contains(&values.len()) {
932                return Err(
933                    "std.native.Result/synchronize expects a value and optional options map".into(),
934                );
935            }
936            let value = values[0].clone();
937            let options = values.get(1).cloned();
938            let (timeout, context) = result_synchronize_options(options)?;
939            native_result::synchronize_value(value, timeout, context)
940        }
941        "success?" | "error?" | "status" | "data" | "error-value" | "context" => {
942            if values.len() != 1 {
943                return Err(format!("std.native.Result/{operation} expects one value"));
944            }
945            let value = values[0].clone();
946            let Value::Result(result) = value else {
947                if matches!(operation, "success?" | "error?") {
948                    return Ok(Value::Bool(false));
949                }
950                return Err(format!("std.native.Result/{operation} expects a Result"));
951            };
952            Ok(match operation {
953                "success?" => Value::Bool(result.is_success()),
954                "error?" => Value::Bool(result.is_error()),
955                "status" => result.status_value(),
956                "data" => result.data.clone(),
957                "error-value" => result.error_value(),
958                "context" => {
959                    if map_entries(&result.context).is_some_and(|entries| entries.is_empty()) {
960                        Value::Nil
961                    } else {
962                        result.context.clone()
963                    }
964                }
965                _ => unreachable!(),
966            })
967        }
968        "with-context" => {
969            if values.len() != 2 {
970                return Err("std.native.Result/with-context expects a Result and context".into());
971            }
972            let value = values[0].clone();
973            let Value::Result(result) = value else {
974                return Err("std.native.Result/with-context expects a Result".into());
975            };
976            let context = values[1].clone();
977            Ok(Value::Result(Rc::new(result.with_context(context)?)))
978        }
979        _ => Err(format!("unknown std.native.Result operation: {operation}")),
980    }
981}
982
983fn native_exception_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
984    let operation = operation
985        .strip_prefix("std.native.Exception/")
986        .unwrap_or(operation);
987    match operation {
988        "new" => {
989            if !(2..=3).contains(&values.len()) {
990                return Err(
991                    "std.native.Exception/new expects a message, data map, and optional cause"
992                        .into(),
993                );
994            }
995            let Value::String(message) = &values[0] else {
996                return Err("std.native.Exception/new expects a string message".into());
997            };
998            if map_entries(&values[1]).is_none() {
999                return Err("std.native.Exception/new expects a data map".into());
1000            }
1001            Ok(Value::ExceptionInfo(Rc::new(ExceptionInfo {
1002                message: message.clone(),
1003                data: Box::new(values[1].clone()),
1004                cause: values.get(2).cloned().map(Box::new),
1005                provenance: Rc::new(RefCell::new(Default::default())),
1006            })))
1007        }
1008        "message" => {
1009            if values.len() != 1 {
1010                return Err("std.native.Exception/message expects one value".into());
1011            }
1012            Ok(match &values[0] {
1013                Value::ExceptionInfo(value) => Value::String(value.message.clone()),
1014                Value::String(value) => Value::String(value.clone()),
1015                value => Value::String(value.display()),
1016            })
1017        }
1018        "class" => {
1019            if values.len() != 1 {
1020                return Err("std.native.Exception/class expects one value".into());
1021            }
1022            Ok(Value::String(portable_type_name(&values[0]).into()))
1023        }
1024        _ => Err(format!("unknown native exception operation: {operation}")),
1025    }
1026}
1027
1028fn value_index(value: &Value) -> Result<usize, String> {
1029    numeric::to_usize_exact(value)
1030        .map_err(|_| "index must be a non-negative host-sized integer".into())
1031}
1032
1033fn value_u64_integer(value: &Value, operation: &str) -> Result<u64, String> {
1034    numeric::to_u64_exact(value)
1035        .map_err(|_| format!("{operation} expects a non-negative 64-bit integer"))
1036}
1037
1038fn value_u16_integer(value: &Value, operation: &str, allow_zero: bool) -> Result<u16, String> {
1039    let value =
1040        numeric::to_u16_exact(value).map_err(|_| format!("{operation} expects a valid port"))?;
1041    if !allow_zero && value == 0 {
1042        return Err(format!("{operation} expects a valid port"));
1043    }
1044    Ok(value)
1045}