Skip to main content

monkey_asm/
runtime_core.rs

1//! Storage/execution agnostic runtime semantics (design §5.2, §8, §10.1).
2//!
3//! Everything here is shared between the native runtime (`runtime.rs`) and
4//! the wasm simulator: tagged value encoding, the frozen semantics matrix
5//! (checked `i64` arithmetic, equality, truthiness, indexing, builtins),
6//! the canonical observer encoding, and call/construct dispatch. Failures
7//! never exit the process at this layer; they are returned as stable
8//! [`RuntimeErrorKind`] categories.
9
10use std::collections::HashMap;
11
12use object::builtins::{BuiltIns, BuiltinId};
13
14use crate::runtime_backend::{CodeHandle, ValueStore};
15
16/// 64-bit tagged value (design §5.2).
17pub type Value = u64;
18
19pub const FALSE_VALUE: Value = 0b0011;
20pub const TRUE_VALUE: Value = 0b0111;
21pub const NULL_VALUE: Value = 0b1011;
22
23pub const PTR_TAG_MASK: u64 = 0b111;
24pub const HEAP_TAG: u64 = 0b001;
25pub const BUILTIN_TAG: u64 = 0b101;
26
27/// SMIs cover `[-2^62, 2^62 - 1]`; the rest of the `i64` range is boxed.
28pub const SMI_MIN: i64 = -(1 << 62);
29pub const SMI_MAX: i64 = (1 << 62) - 1;
30
31/// Hard cap shared with the calling convention: a closure body receives its
32/// arguments in `x1..x7` (design §2.2, §7).
33pub const MAX_INVOKE_ARGS: usize = 7;
34
35pub fn is_smi(value: Value) -> bool {
36    value & 1 == 0
37}
38
39pub fn smi_to_i64(value: Value) -> i64 {
40    (value as i64) >> 1
41}
42
43pub fn i64_fits_smi(raw: i64) -> bool {
44    (SMI_MIN..=SMI_MAX).contains(&raw)
45}
46
47pub fn smi_from_i64(raw: i64) -> Value {
48    debug_assert!(i64_fits_smi(raw));
49    (raw << 1) as u64
50}
51
52pub fn is_heap(value: Value) -> bool {
53    value & PTR_TAG_MASK == HEAP_TAG
54}
55
56pub fn is_builtin(value: Value) -> bool {
57    value & PTR_TAG_MASK == BUILTIN_TAG
58}
59
60pub fn bool_value(value: bool) -> Value {
61    if value {
62        TRUE_VALUE
63    } else {
64        FALSE_VALUE
65    }
66}
67
68/// Frozen builtin numbering used by the immediate encoding
69/// `(ordinal << 3) | 0b101`. `print` shares `BuiltinId::Puts` and therefore
70/// the same ordinal (see `object/builtins.rs`).
71pub fn builtin_ordinal(id: BuiltinId) -> u64 {
72    match id {
73        BuiltinId::Len => 0,
74        BuiltinId::Puts => 1,
75        BuiltinId::First => 2,
76        BuiltinId::Last => 3,
77        BuiltinId::Rest => 4,
78        BuiltinId::Push => 5,
79    }
80}
81
82pub fn builtin_from_ordinal(ordinal: u64) -> Option<BuiltinId> {
83    match ordinal {
84        0 => Some(BuiltinId::Len),
85        1 => Some(BuiltinId::Puts),
86        2 => Some(BuiltinId::First),
87        3 => Some(BuiltinId::Last),
88        4 => Some(BuiltinId::Rest),
89        5 => Some(BuiltinId::Push),
90        _ => None,
91    }
92}
93
94pub fn builtin_value(id: BuiltinId) -> Value {
95    (builtin_ordinal(id) << 3) | BUILTIN_TAG
96}
97
98/// Canonical name used by the observer protocol (`print` normalizes to
99/// `puts`).
100pub fn builtin_canonical_name(id: BuiltinId) -> &'static str {
101    match id {
102        BuiltinId::Len => "len",
103        BuiltinId::Puts => "puts",
104        BuiltinId::First => "first",
105        BuiltinId::Last => "last",
106        BuiltinId::Rest => "rest",
107        BuiltinId::Push => "push",
108    }
109}
110
111/// Builtin id for a `SymbolScope::Builtin` symbol index (the index into
112/// `object::builtins::BuiltIns`, where `print` aliases `puts`).
113pub fn builtin_id_for_symbol_index(index: usize) -> Option<BuiltinId> {
114    BuiltIns.get(index).map(|definition| definition.id)
115}
116
117/// Stable error categories, frozen ABI between `.s` and the runtime
118/// (design §8). Tests compare kinds, never messages.
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120#[repr(u64)]
121pub enum RuntimeErrorKind {
122    InternalError = 0,
123    TypeError = 1,
124    ArityError = 2,
125    NotCallable = 3,
126    NotConstructable = 4,
127    MissingProperty = 5,
128    InvalidHashKey = 6,
129    DivisionByZero = 7,
130    IntegerOverflow = 8,
131    ResourceLimit = 9,
132}
133
134impl RuntimeErrorKind {
135    pub fn name(self) -> &'static str {
136        match self {
137            RuntimeErrorKind::InternalError => "InternalError",
138            RuntimeErrorKind::TypeError => "TypeError",
139            RuntimeErrorKind::ArityError => "ArityError",
140            RuntimeErrorKind::NotCallable => "NotCallable",
141            RuntimeErrorKind::NotConstructable => "NotConstructable",
142            RuntimeErrorKind::MissingProperty => "MissingProperty",
143            RuntimeErrorKind::InvalidHashKey => "InvalidHashKey",
144            RuntimeErrorKind::DivisionByZero => "DivisionByZero",
145            RuntimeErrorKind::IntegerOverflow => "IntegerOverflow",
146            RuntimeErrorKind::ResourceLimit => "ResourceLimit",
147        }
148    }
149
150    pub fn from_u64(kind: u64) -> Option<RuntimeErrorKind> {
151        match kind {
152            0 => Some(RuntimeErrorKind::InternalError),
153            1 => Some(RuntimeErrorKind::TypeError),
154            2 => Some(RuntimeErrorKind::ArityError),
155            3 => Some(RuntimeErrorKind::NotCallable),
156            4 => Some(RuntimeErrorKind::NotConstructable),
157            5 => Some(RuntimeErrorKind::MissingProperty),
158            6 => Some(RuntimeErrorKind::InvalidHashKey),
159            7 => Some(RuntimeErrorKind::DivisionByZero),
160            8 => Some(RuntimeErrorKind::IntegerOverflow),
161            9 => Some(RuntimeErrorKind::ResourceLimit),
162            _ => None,
163        }
164    }
165}
166
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub struct RuntimeFailure {
169    pub kind: RuntimeErrorKind,
170    pub message: String,
171}
172
173pub type RuntimeResult<T> = Result<T, RuntimeFailure>;
174
175fn fail<T>(kind: RuntimeErrorKind, message: impl Into<String>) -> RuntimeResult<T> {
176    Err(RuntimeFailure {
177        kind,
178        message: message.into(),
179    })
180}
181
182#[derive(Clone, Debug, Eq, PartialEq, Hash)]
183pub enum HashKey {
184    Integer(i64),
185    Boolean(bool),
186    Str(String),
187}
188
189impl HashKey {
190    /// Rank + canonical bytes ordering used by displays and the observer
191    /// (integer=0, boolean=1, string=2; design §10.2).
192    fn rank(&self) -> u8 {
193        match self {
194            HashKey::Integer(_) => 0,
195            HashKey::Boolean(_) => 1,
196            HashKey::Str(_) => 2,
197        }
198    }
199
200    fn canonical_bytes(&self) -> Vec<u8> {
201        match self {
202            HashKey::Integer(raw) => raw.to_string().into_bytes(),
203            HashKey::Boolean(raw) => raw.to_string().into_bytes(),
204            HashKey::Str(raw) => raw.clone().into_bytes(),
205        }
206    }
207}
208
209#[derive(Clone, Debug)]
210pub struct ClosureData {
211    pub code: CodeHandle,
212    pub num_parameters: u64,
213    pub free: Vec<Value>,
214}
215
216#[derive(Clone, Debug)]
217pub struct ClassData {
218    pub name: String,
219    pub methods: HashMap<String, Value>,
220    pub constructor: Option<Value>,
221}
222
223#[derive(Clone, Debug)]
224pub struct InstanceData {
225    pub class: Value,
226    pub fields: HashMap<String, Value>,
227}
228
229#[derive(Clone, Debug)]
230pub struct BoundMethodData {
231    pub receiver: Value,
232    pub method: Value,
233    pub name: String,
234}
235
236/// Heap object layout is fully opaque to generated assembly (design §5.2),
237/// which is what lets it be plain Rust data.
238#[derive(Clone, Debug)]
239pub enum HeapObject {
240    BoxedInt(i64),
241    Str(String),
242    Array(Vec<Value>),
243    Hash(HashMap<HashKey, Value>),
244    Closure(ClosureData),
245    Class(ClassData),
246    Instance(InstanceData),
247    BoundMethod(BoundMethodData),
248}
249
250fn get_obj<S: ValueStore>(store: &S, value: Value) -> RuntimeResult<&HeapObject> {
251    match store.try_get(value) {
252        Some(object) => Ok(object),
253        None => fail(RuntimeErrorKind::InternalError, "invalid heap reference"),
254    }
255}
256
257/// Raw integer behind either representation (SMI or boxed).
258pub fn int_value<S: ValueStore>(store: &S, value: Value) -> Option<i64> {
259    if is_smi(value) {
260        return Some(smi_to_i64(value));
261    }
262    match store.try_get(value) {
263        Some(HeapObject::BoxedInt(raw)) => Some(*raw),
264        _ => None,
265    }
266}
267
268/// SMI when it fits, boxed integer otherwise; results shrink back to SMI
269/// whenever possible.
270pub fn make_int<S: ValueStore>(store: &mut S, raw: i64) -> Value {
271    if i64_fits_smi(raw) {
272        smi_from_i64(raw)
273    } else {
274        store.alloc(HeapObject::BoxedInt(raw))
275    }
276}
277
278/// Frozen truthiness: only `false` and `null` are falsy (design §10.1).
279pub fn truthy(value: Value) -> bool {
280    value != FALSE_VALUE && value != NULL_VALUE
281}
282
283pub fn string_from_utf8<S: ValueStore>(store: &mut S, bytes: &[u8]) -> RuntimeResult<Value> {
284    match std::str::from_utf8(bytes) {
285        Ok(text) => Ok(store.alloc(HeapObject::Str(text.to_string()))),
286        Err(_) => fail(RuntimeErrorKind::InternalError, "string literal is not valid UTF-8"),
287    }
288}
289
290pub fn array_from_values<S: ValueStore>(store: &mut S, values: &[Value]) -> Value {
291    store.alloc(HeapObject::Array(values.to_vec()))
292}
293
294pub fn hash_key<S: ValueStore>(store: &S, value: Value) -> Option<HashKey> {
295    if let Some(raw) = int_value(store, value) {
296        return Some(HashKey::Integer(raw));
297    }
298    match value {
299        TRUE_VALUE => Some(HashKey::Boolean(true)),
300        FALSE_VALUE => Some(HashKey::Boolean(false)),
301        _ => match store.try_get(value) {
302            Some(HeapObject::Str(text)) => Some(HashKey::Str(text.clone())),
303            _ => None,
304        },
305    }
306}
307
308/// `pairs` is `k0,v0,k1,v1,…`; later duplicates win, like the VMs.
309pub fn hash_from_pairs<S: ValueStore>(store: &mut S, pairs: &[Value]) -> RuntimeResult<Value> {
310    debug_assert_eq!(pairs.len() % 2, 0);
311    let mut entries = HashMap::new();
312    for &[key, value] in pairs.as_chunks::<2>().0 {
313        let key = match hash_key(store, key) {
314            Some(key) => key,
315            None => {
316                let shown = display(store, key)?;
317                return fail(
318                    RuntimeErrorKind::InvalidHashKey,
319                    format!("hash key must be hashable, got {}", shown),
320                );
321            }
322        };
323        entries.insert(key, value);
324    }
325    Ok(store.alloc(HeapObject::Hash(entries)))
326}
327
328pub fn closure_new<S: ValueStore>(
329    store: &mut S,
330    code: CodeHandle,
331    num_parameters: u64,
332    free: &[Value],
333) -> RuntimeResult<Value> {
334    if num_parameters as usize > MAX_INVOKE_ARGS {
335        return fail(
336            RuntimeErrorKind::ResourceLimit,
337            format!("closure cannot take more than {} parameters", MAX_INVOKE_ARGS),
338        );
339    }
340    Ok(store.alloc(HeapObject::Closure(ClosureData {
341        code,
342        num_parameters,
343        free: free.to_vec(),
344    })))
345}
346
347/// v1's only free-variable access path (design §7).
348pub fn get_free<S: ValueStore>(store: &S, closure: Value, index: u64) -> RuntimeResult<Value> {
349    match get_obj(store, closure)? {
350        HeapObject::Closure(data) => match data.free.get(index as usize) {
351            Some(value) => Ok(*value),
352            None => fail(RuntimeErrorKind::InternalError, "free variable index out of range"),
353        },
354        _ => fail(RuntimeErrorKind::InternalError, "rt_get_free on a non-closure"),
355    }
356}
357
358pub fn class_new<S: ValueStore>(store: &mut S, name: &str) -> Value {
359    store.alloc(HeapObject::Class(ClassData {
360        name: name.to_string(),
361        methods: HashMap::new(),
362        constructor: None,
363    }))
364}
365
366pub fn class_add_method<S: ValueStore>(
367    store: &mut S,
368    class: Value,
369    name: &str,
370    method: Value,
371    is_constructor: bool,
372) -> RuntimeResult<()> {
373    if !matches!(get_obj(store, method)?, HeapObject::Closure(_)) {
374        return fail(RuntimeErrorKind::InternalError, "class method must be a closure");
375    }
376    match store.try_get_mut(class) {
377        Some(HeapObject::Class(data)) => {
378            if is_constructor {
379                data.constructor = Some(method);
380            } else {
381                data.methods.insert(name.to_string(), method);
382            }
383            Ok(())
384        }
385        _ => fail(RuntimeErrorKind::InternalError, "cannot install a method on a non-class"),
386    }
387}
388
389/// Field first, then a freshly bound method; missing → `MissingProperty`
390/// (design §8, matching the VMs' error surface).
391pub fn get_property<S: ValueStore>(
392    store: &mut S,
393    object: Value,
394    name: &str,
395) -> RuntimeResult<Value> {
396    let (class, field) = match store.try_get(object) {
397        Some(HeapObject::Instance(instance)) => {
398            (instance.class, instance.fields.get(name).copied())
399        }
400        _ => {
401            let shown = display(store, object)?;
402            return fail(
403                RuntimeErrorKind::TypeError,
404                format!("cannot read property '{}' of {}", name, shown),
405            );
406        }
407    };
408    if let Some(field) = field {
409        return Ok(field);
410    }
411    let (class_name, method) = match get_obj(store, class)? {
412        HeapObject::Class(data) => (data.name.clone(), data.methods.get(name).copied()),
413        _ => return fail(RuntimeErrorKind::InternalError, "instance has an invalid class"),
414    };
415    match method {
416        Some(method) => Ok(store.alloc(HeapObject::BoundMethod(BoundMethodData {
417            receiver: object,
418            method,
419            name: name.to_string(),
420        }))),
421        None => fail(
422            RuntimeErrorKind::MissingProperty,
423            format!("property '{}' does not exist on {}", name, class_name),
424        ),
425    }
426}
427
428pub fn set_property<S: ValueStore>(
429    store: &mut S,
430    object: Value,
431    name: &str,
432    value: Value,
433) -> RuntimeResult<()> {
434    match store.try_get_mut(object) {
435        Some(HeapObject::Instance(instance)) => {
436            instance.fields.insert(name.to_string(), value);
437            Ok(())
438        }
439        _ => {
440            let shown = display(store, object)?;
441            fail(
442                RuntimeErrorKind::TypeError,
443                format!("cannot set property '{}' of {}", name, shown),
444            )
445        }
446    }
447}
448
449/// Array out-of-bounds and hash missing key are `null`; wrong container or
450/// index types are `TypeError`; unhashable hash keys are `InvalidHashKey`
451/// (design §10.1).
452pub fn index<S: ValueStore>(store: &S, object: Value, index: Value) -> RuntimeResult<Value> {
453    if is_heap(object) {
454        match get_obj(store, object)? {
455            HeapObject::Array(elements) => {
456                if let Some(position) = int_value(store, index) {
457                    if position >= 0 && (position as usize) < elements.len() {
458                        return Ok(elements[position as usize]);
459                    }
460                    return Ok(NULL_VALUE);
461                }
462            }
463            HeapObject::Hash(entries) => {
464                let key = match hash_key(store, index) {
465                    Some(key) => key,
466                    None => {
467                        return fail(RuntimeErrorKind::InvalidHashKey, "unsupported hash index key")
468                    }
469                };
470                return Ok(entries.get(&key).copied().unwrap_or(NULL_VALUE));
471            }
472            _ => {}
473        }
474    }
475    let object_shown = display(store, object)?;
476    let index_shown = display(store, index)?;
477    fail(
478        RuntimeErrorKind::TypeError,
479        format!("unsupported index operation for {} and {}", object_shown, index_shown),
480    )
481}
482
483/// Frozen equality matrix (design §10.1): integers by raw value, scalars by
484/// value, builtins by id, aggregates recursively, identity types by object
485/// identity, differing types compare unequal.
486pub fn eq_values<S: ValueStore>(store: &S, left: Value, right: Value) -> RuntimeResult<bool> {
487    let left_int = int_value(store, left);
488    let right_int = int_value(store, right);
489    if let (Some(l), Some(r)) = (left_int, right_int) {
490        return Ok(l == r);
491    }
492    if left_int.is_some() != right_int.is_some() {
493        return Ok(false);
494    }
495    if !is_heap(left) || !is_heap(right) {
496        // true/false/null singletons and builtin immediates: bit equality is
497        // value (respectively id) equality; mixed immediate/heap is unequal.
498        return Ok(left == right);
499    }
500    match (get_obj(store, left)?, get_obj(store, right)?) {
501        (HeapObject::Str(l), HeapObject::Str(r)) => Ok(l == r),
502        (HeapObject::Array(l), HeapObject::Array(r)) => {
503            if l.len() != r.len() {
504                return Ok(false);
505            }
506            for (l_element, r_element) in l.iter().zip(r.iter()) {
507                if !eq_values(store, *l_element, *r_element)? {
508                    return Ok(false);
509                }
510            }
511            Ok(true)
512        }
513        (HeapObject::Hash(l), HeapObject::Hash(r)) => {
514            if l.len() != r.len() {
515                return Ok(false);
516            }
517            for (key, l_value) in l.iter() {
518                match r.get(key) {
519                    Some(r_value) => {
520                        if !eq_values(store, *l_value, *r_value)? {
521                            return Ok(false);
522                        }
523                    }
524                    None => return Ok(false),
525                }
526            }
527            Ok(true)
528        }
529        (HeapObject::Closure(_), HeapObject::Closure(_))
530        | (HeapObject::Class(_), HeapObject::Class(_))
531        | (HeapObject::Instance(_), HeapObject::Instance(_))
532        | (HeapObject::BoundMethod(_), HeapObject::BoundMethod(_)) => Ok(left == right),
533        _ => Ok(false),
534    }
535}
536
537/// `>` accepts integers only (design §10.1).
538pub fn gt<S: ValueStore>(store: &S, left: Value, right: Value) -> RuntimeResult<Value> {
539    if let (Some(l), Some(r)) = (int_value(store, left), int_value(store, right)) {
540        return Ok(bool_value(l > r));
541    }
542    let left_shown = display(store, left)?;
543    let right_shown = display(store, right)?;
544    fail(
545        RuntimeErrorKind::TypeError,
546        format!("unsupported comparison for {} and {}", left_shown, right_shown),
547    )
548}
549
550fn checked_arith<S: ValueStore>(
551    store: &mut S,
552    left: Value,
553    right: Value,
554    operation: &str,
555    apply: impl Fn(i64, i64) -> Option<i64>,
556) -> RuntimeResult<Value> {
557    if let (Some(l), Some(r)) = (int_value(store, left), int_value(store, right)) {
558        return match apply(l, r) {
559            Some(raw) => Ok(make_int(store, raw)),
560            None => fail(
561                RuntimeErrorKind::IntegerOverflow,
562                format!("integer overflow in {}", operation),
563            ),
564        };
565    }
566    let left_shown = display(store, left)?;
567    let right_shown = display(store, right)?;
568    fail(
569        RuntimeErrorKind::TypeError,
570        format!("unsupported binary operation for {} and {}", left_shown, right_shown),
571    )
572}
573
574/// Checked addition; also string concatenation (design §8).
575pub fn add<S: ValueStore>(store: &mut S, left: Value, right: Value) -> RuntimeResult<Value> {
576    if int_value(store, left).is_none() {
577        if let (Some(HeapObject::Str(l)), Some(HeapObject::Str(r))) =
578            (store.try_get(left), store.try_get(right))
579        {
580            let combined = format!("{}{}", l, r);
581            return Ok(store.alloc(HeapObject::Str(combined)));
582        }
583    }
584    checked_arith(store, left, right, "addition", i64::checked_add)
585}
586
587pub fn sub<S: ValueStore>(store: &mut S, left: Value, right: Value) -> RuntimeResult<Value> {
588    checked_arith(store, left, right, "subtraction", i64::checked_sub)
589}
590
591pub fn mul<S: ValueStore>(store: &mut S, left: Value, right: Value) -> RuntimeResult<Value> {
592    checked_arith(store, left, right, "multiplication", i64::checked_mul)
593}
594
595/// Division separates `DivisionByZero` first, then truncates toward zero via
596/// checked `i64` division (`i64::MIN / -1` → `IntegerOverflow`).
597pub fn div<S: ValueStore>(store: &mut S, left: Value, right: Value) -> RuntimeResult<Value> {
598    if let (Some(_), Some(0)) = (int_value(store, left), int_value(store, right)) {
599        return fail(RuntimeErrorKind::DivisionByZero, "division by zero");
600    }
601    checked_arith(store, left, right, "division", i64::checked_div)
602}
603
604pub fn minus<S: ValueStore>(store: &mut S, value: Value) -> RuntimeResult<Value> {
605    if let Some(raw) = int_value(store, value) {
606        return match raw.checked_neg() {
607            Some(negated) => Ok(make_int(store, negated)),
608            None => fail(RuntimeErrorKind::IntegerOverflow, "integer overflow in negation"),
609        };
610    }
611    let shown = display(store, value)?;
612    fail(RuntimeErrorKind::TypeError, format!("unsupported type for negation: {}", shown))
613}
614
615/// `!v` is strictly the logical inverse of truthiness (design §10.1).
616pub fn bang(value: Value) -> Value {
617    bool_value(!truthy(value))
618}
619
620/// Hash entries in canonical order: `(key type rank, canonical key bytes)`.
621fn sorted_hash_entries(entries: &HashMap<HashKey, Value>) -> Vec<(&HashKey, Value)> {
622    let mut sorted: Vec<(&HashKey, Value)> =
623        entries.iter().map(|(key, value)| (key, *value)).collect();
624    sorted.sort_by(|(a, _), (b, _)| {
625        (a.rank(), a.canonical_bytes()).cmp(&(b.rank(), b.canonical_bytes()))
626    });
627    sorted
628}
629
630fn key_display(key: &HashKey) -> String {
631    match key {
632        HashKey::Integer(raw) => raw.to_string(),
633        HashKey::Boolean(raw) => raw.to_string(),
634        HashKey::Str(raw) => raw.clone(),
635    }
636}
637
638fn instance_class_name<S: ValueStore>(store: &S, instance: Value) -> RuntimeResult<String> {
639    match get_obj(store, instance)? {
640        HeapObject::Instance(data) => match get_obj(store, data.class)? {
641            HeapObject::Class(class) => Ok(class.name.clone()),
642            _ => fail(RuntimeErrorKind::InternalError, "instance has an invalid class"),
643        },
644        _ => fail(RuntimeErrorKind::InternalError, "expected an instance"),
645    }
646}
647
648/// Shared language display (design §10.2): what `puts` prints.
649pub fn display<S: ValueStore>(store: &S, value: Value) -> RuntimeResult<String> {
650    if let Some(raw) = int_value(store, value) {
651        return Ok(raw.to_string());
652    }
653    match value {
654        TRUE_VALUE => return Ok("true".to_string()),
655        FALSE_VALUE => return Ok("false".to_string()),
656        NULL_VALUE => return Ok("null".to_string()),
657        _ => {}
658    }
659    if is_builtin(value) {
660        return Ok("[builtin function]".to_string());
661    }
662    match get_obj(store, value)? {
663        HeapObject::BoxedInt(_) => unreachable!("handled by int_value"),
664        HeapObject::Str(text) => Ok(text.clone()),
665        HeapObject::Array(elements) => {
666            let mut rendered = Vec::with_capacity(elements.len());
667            for element in elements {
668                rendered.push(display(store, *element)?);
669            }
670            Ok(format!("[{}]", rendered.join(", ")))
671        }
672        HeapObject::Hash(entries) => {
673            let mut rendered = Vec::with_capacity(entries.len());
674            for (key, entry_value) in sorted_hash_entries(entries) {
675                rendered.push(format!("{}: {}", key_display(key), display(store, entry_value)?));
676            }
677            Ok(format!("{{{}}}", rendered.join(", ")))
678        }
679        HeapObject::Closure(_) => Ok("[function]".to_string()),
680        HeapObject::Class(data) => Ok(format!("[class {}]", data.name)),
681        HeapObject::Instance(_) => Ok(format!("[object {}]", instance_class_name(store, value)?)),
682        HeapObject::BoundMethod(data) => Ok(format!(
683            "[bound method {}.{}]",
684            instance_class_name(store, data.receiver)?,
685            data.name
686        )),
687    }
688}
689
690fn json_escape(text: &str) -> String {
691    let mut escaped = String::with_capacity(text.len() + 2);
692    for character in text.chars() {
693        match character {
694            '"' => escaped.push_str("\\\""),
695            '\\' => escaped.push_str("\\\\"),
696            '\n' => escaped.push_str("\\n"),
697            '\r' => escaped.push_str("\\r"),
698            '\t' => escaped.push_str("\\t"),
699            c if (c as u32) < 0x20 => {
700                escaped.push_str(&format!("\\u{:04x}", c as u32));
701            }
702            c => escaped.push(c),
703        }
704    }
705    escaped
706}
707
708/// Canonical tagged JSON encoding for the observer protocol (design §10.2).
709pub fn canonical_value<S: ValueStore>(store: &S, value: Value) -> RuntimeResult<String> {
710    if let Some(raw) = int_value(store, value) {
711        return Ok(format!("{{\"type\":\"integer\",\"value\":\"{}\"}}", raw));
712    }
713    match value {
714        TRUE_VALUE => return Ok("{\"type\":\"boolean\",\"value\":true}".to_string()),
715        FALSE_VALUE => return Ok("{\"type\":\"boolean\",\"value\":false}".to_string()),
716        NULL_VALUE => return Ok("{\"type\":\"null\"}".to_string()),
717        _ => {}
718    }
719    if is_builtin(value) {
720        let id = match builtin_from_ordinal(value >> 3) {
721            Some(id) => id,
722            None => return fail(RuntimeErrorKind::InternalError, "invalid builtin encoding"),
723        };
724        return Ok(format!("{{\"type\":\"builtin\",\"id\":\"{}\"}}", builtin_canonical_name(id)));
725    }
726    match get_obj(store, value)? {
727        HeapObject::BoxedInt(_) => unreachable!("handled by int_value"),
728        HeapObject::Str(text) => {
729            Ok(format!("{{\"type\":\"string\",\"value\":\"{}\"}}", json_escape(text)))
730        }
731        HeapObject::Array(elements) => {
732            let mut rendered = Vec::with_capacity(elements.len());
733            for element in elements {
734                rendered.push(canonical_value(store, *element)?);
735            }
736            Ok(format!("{{\"type\":\"array\",\"elements\":[{}]}}", rendered.join(",")))
737        }
738        HeapObject::Hash(entries) => {
739            let mut rendered = Vec::with_capacity(entries.len());
740            for (key, entry_value) in sorted_hash_entries(entries) {
741                let key_json = match key {
742                    HashKey::Integer(raw) => {
743                        format!("{{\"type\":\"integer\",\"value\":\"{}\"}}", raw)
744                    }
745                    HashKey::Boolean(raw) => {
746                        format!("{{\"type\":\"boolean\",\"value\":{}}}", raw)
747                    }
748                    HashKey::Str(raw) => {
749                        format!("{{\"type\":\"string\",\"value\":\"{}\"}}", json_escape(raw))
750                    }
751                };
752                rendered.push(format!(
753                    "{{\"key\":{},\"value\":{}}}",
754                    key_json,
755                    canonical_value(store, entry_value)?
756                ));
757            }
758            Ok(format!("{{\"type\":\"hash\",\"entries\":[{}]}}", rendered.join(",")))
759        }
760        HeapObject::Closure(_) => Ok("{\"type\":\"function\"}".to_string()),
761        HeapObject::Class(data) => {
762            Ok(format!("{{\"type\":\"class\",\"name\":\"{}\"}}", json_escape(&data.name)))
763        }
764        HeapObject::Instance(_) => Ok(format!(
765            "{{\"type\":\"instance\",\"class\":\"{}\"}}",
766            json_escape(&instance_class_name(store, value)?)
767        )),
768        HeapObject::BoundMethod(data) => Ok(format!(
769            "{{\"type\":\"bound_method\",\"class\":\"{}\",\"method\":\"{}\"}}",
770            json_escape(&instance_class_name(store, data.receiver)?),
771            json_escape(&data.name)
772        )),
773    }
774}
775
776/// Where `puts`/`print` bytes go: stdout natively, a buffer in the simulator
777/// and in tests (design §8.1).
778pub trait OutputSink {
779    fn write_line(&mut self, line: &str);
780}
781
782/// Test/simulator sink collecting raw stdout bytes.
783pub struct BufferSink {
784    pub bytes: Vec<u8>,
785}
786
787impl Default for BufferSink {
788    fn default() -> Self {
789        Self::new()
790    }
791}
792
793impl BufferSink {
794    pub fn new() -> BufferSink {
795        BufferSink {
796            bytes: vec![],
797        }
798    }
799}
800
801impl OutputSink for BufferSink {
802    fn write_line(&mut self, line: &str) {
803        self.bytes.extend_from_slice(line.as_bytes());
804        self.bytes.push(b'\n');
805    }
806}
807
808/// Builtins run inside the runtime (no separate FFI symbols); arity/type
809/// problems are terminating errors, never `Error` values (design §8).
810pub fn call_builtin<S: ValueStore>(
811    store: &mut S,
812    sink: &mut dyn OutputSink,
813    id: BuiltinId,
814    args: &[Value],
815) -> RuntimeResult<Value> {
816    let expect_arity = |count: usize| -> RuntimeResult<()> {
817        if args.len() != count {
818            return fail(
819                RuntimeErrorKind::ArityError,
820                format!(
821                    "builtin {} expected {} argument{}, got {}",
822                    builtin_canonical_name(id),
823                    count,
824                    if count == 1 { "" } else { "s" },
825                    args.len()
826                ),
827            );
828        }
829        Ok(())
830    };
831    match id {
832        BuiltinId::Len => {
833            expect_arity(1)?;
834            let length = match store.try_get(args[0]) {
835                Some(HeapObject::Str(text)) => text.len() as i64,
836                Some(HeapObject::Array(elements)) => elements.len() as i64,
837                _ => {
838                    let shown = display(store, args[0])?;
839                    return fail(
840                        RuntimeErrorKind::TypeError,
841                        format!("builtin len not supported for type {}", shown),
842                    );
843                }
844            };
845            Ok(make_int(store, length))
846        }
847        BuiltinId::Puts => {
848            for argument in args {
849                let line = display(store, *argument)?;
850                sink.write_line(&line);
851            }
852            Ok(NULL_VALUE)
853        }
854        BuiltinId::First | BuiltinId::Last => {
855            expect_arity(1)?;
856            match store.try_get(args[0]) {
857                Some(HeapObject::Array(elements)) => {
858                    let element =
859                        if id == BuiltinId::First { elements.first() } else { elements.last() };
860                    Ok(element.copied().unwrap_or(NULL_VALUE))
861                }
862                _ => {
863                    let shown = display(store, args[0])?;
864                    fail(
865                        RuntimeErrorKind::TypeError,
866                        format!(
867                            "builtin {} not supported for type {}",
868                            builtin_canonical_name(id),
869                            shown
870                        ),
871                    )
872                }
873            }
874        }
875        BuiltinId::Rest => {
876            expect_arity(1)?;
877            let rest = match store.try_get(args[0]) {
878                Some(HeapObject::Array(elements)) => {
879                    if elements.is_empty() {
880                        return Ok(NULL_VALUE);
881                    }
882                    elements[1..].to_vec()
883                }
884                _ => {
885                    let shown = display(store, args[0])?;
886                    return fail(
887                        RuntimeErrorKind::TypeError,
888                        format!("builtin rest not supported for type {}", shown),
889                    );
890                }
891            };
892            Ok(store.alloc(HeapObject::Array(rest)))
893        }
894        BuiltinId::Push => {
895            expect_arity(2)?;
896            let pushed = match store.try_get(args[0]) {
897                Some(HeapObject::Array(elements)) => {
898                    let mut extended = elements.clone();
899                    extended.push(args[1]);
900                    extended
901                }
902                _ => {
903                    let shown = display(store, args[0])?;
904                    return fail(
905                        RuntimeErrorKind::TypeError,
906                        format!("builtin push not supported for type {}", shown),
907                    );
908                }
909            };
910            Ok(store.alloc(HeapObject::Array(pushed)))
911        }
912    }
913}
914
915/// How the execution adapter finishes an `Invoke` (design §8.1):
916/// constructors always yield their instance.
917#[derive(Clone, Debug, PartialEq)]
918pub enum ReturnPolicy {
919    Direct,
920    ConstructorInstance(Value),
921}
922
923/// Dispatch result: builtins resolve immediately, code invocations bounce
924/// back through the execution adapter (real function pointer natively, a
925/// simulated PC in wasm).
926#[derive(Clone, Debug)]
927pub enum CallDispatch {
928    Return(Value),
929    Invoke { code: CodeHandle, closure: Value, args: Vec<Value>, return_policy: ReturnPolicy },
930}
931
932fn closure_signature<S: ValueStore>(store: &S, closure: Value) -> Option<(CodeHandle, u64)> {
933    match store.try_get(closure) {
934        Some(HeapObject::Closure(data)) => Some((data.code, data.num_parameters)),
935        _ => None,
936    }
937}
938
939/// Plain-call dispatch (design §7.2): closures, builtins and bound methods
940/// are callable; classes must use `new`.
941pub fn dispatch_call<S: ValueStore>(
942    store: &mut S,
943    sink: &mut dyn OutputSink,
944    callee: Value,
945    args: &[Value],
946) -> RuntimeResult<CallDispatch> {
947    if is_builtin(callee) {
948        let id = match builtin_from_ordinal(callee >> 3) {
949            Some(id) => id,
950            None => return fail(RuntimeErrorKind::InternalError, "invalid builtin encoding"),
951        };
952        return Ok(CallDispatch::Return(call_builtin(store, sink, id, args)?));
953    }
954    if is_heap(callee) {
955        match get_obj(store, callee)? {
956            HeapObject::Closure(data) => {
957                if data.num_parameters != args.len() as u64 {
958                    return fail(
959                        RuntimeErrorKind::ArityError,
960                        format!(
961                            "wrong number of arguments: want={}, got={}",
962                            data.num_parameters,
963                            args.len()
964                        ),
965                    );
966                }
967                return Ok(CallDispatch::Invoke {
968                    code: data.code,
969                    closure: callee,
970                    args: args.to_vec(),
971                    return_policy: ReturnPolicy::Direct,
972                });
973            }
974            HeapObject::BoundMethod(bound) => {
975                let (receiver, method, name) = (bound.receiver, bound.method, bound.name.clone());
976                let (code, num_parameters) = match closure_signature(store, method) {
977                    Some(signature) => signature,
978                    None => {
979                        return fail(
980                            RuntimeErrorKind::InternalError,
981                            "bound method is not a closure",
982                        )
983                    }
984                };
985                let expected = num_parameters.saturating_sub(1);
986                if expected != args.len() as u64 {
987                    let class_name = instance_class_name(store, receiver)?;
988                    return fail(
989                        RuntimeErrorKind::ArityError,
990                        format!(
991                            "wrong number of arguments for {}.{}: want={}, got={}",
992                            class_name,
993                            name,
994                            expected,
995                            args.len()
996                        ),
997                    );
998                }
999                let mut invoke_args = Vec::with_capacity(args.len() + 1);
1000                invoke_args.push(receiver);
1001                invoke_args.extend_from_slice(args);
1002                return Ok(CallDispatch::Invoke {
1003                    code,
1004                    closure: method,
1005                    args: invoke_args,
1006                    return_policy: ReturnPolicy::Direct,
1007                });
1008            }
1009            HeapObject::Class(data) => {
1010                return fail(
1011                    RuntimeErrorKind::NotCallable,
1012                    format!("class {} must be constructed with new", data.name),
1013                );
1014            }
1015            _ => {}
1016        }
1017    }
1018    let shown = display(store, callee)?;
1019    fail(RuntimeErrorKind::NotCallable, format!("cannot call {}", shown))
1020}
1021
1022/// `new` dispatch (design §7.2): callee must be a class; the constructor —
1023/// when present — runs with the fresh instance as `this` and the instance is
1024/// always the result.
1025pub fn dispatch_construct<S: ValueStore>(
1026    store: &mut S,
1027    callee: Value,
1028    args: &[Value],
1029) -> RuntimeResult<CallDispatch> {
1030    let (class_name, constructor) = match store.try_get(callee) {
1031        Some(HeapObject::Class(data)) => (data.name.clone(), data.constructor),
1032        _ => {
1033            let shown = display(store, callee)?;
1034            return fail(RuntimeErrorKind::NotConstructable, format!("cannot construct {}", shown));
1035        }
1036    };
1037    let constructor = match constructor {
1038        None => {
1039            if !args.is_empty() {
1040                return fail(
1041                    RuntimeErrorKind::ArityError,
1042                    format!(
1043                        "wrong number of arguments for {}.constructor: want=0, got={}",
1044                        class_name,
1045                        args.len()
1046                    ),
1047                );
1048            }
1049            let instance = store.alloc(HeapObject::Instance(InstanceData {
1050                class: callee,
1051                fields: HashMap::new(),
1052            }));
1053            return Ok(CallDispatch::Return(instance));
1054        }
1055        Some(constructor) => constructor,
1056    };
1057    let (code, num_parameters) = match closure_signature(store, constructor) {
1058        Some(signature) => signature,
1059        None => return fail(RuntimeErrorKind::InternalError, "constructor is not a closure"),
1060    };
1061    let expected = num_parameters.saturating_sub(1);
1062    if expected != args.len() as u64 {
1063        return fail(
1064            RuntimeErrorKind::ArityError,
1065            format!(
1066                "wrong number of arguments for {}.constructor: want={}, got={}",
1067                class_name,
1068                expected,
1069                args.len()
1070            ),
1071        );
1072    }
1073    let instance = store.alloc(HeapObject::Instance(InstanceData {
1074        class: callee,
1075        fields: HashMap::new(),
1076    }));
1077    let mut invoke_args = Vec::with_capacity(args.len() + 1);
1078    invoke_args.push(instance);
1079    invoke_args.extend_from_slice(args);
1080    Ok(CallDispatch::Invoke {
1081        code,
1082        closure: constructor,
1083        args: invoke_args,
1084        return_policy: ReturnPolicy::ConstructorInstance(instance),
1085    })
1086}