Skip to main content

nanvm_lib/l0/
abi.rs

1//! L0's ABI over handles: step 2 of
2//! [`todo/l0-interface.md`](../../todo/l0-interface.md).
3//!
4//! The functions a compiled program imports from `fjs:l0`, with the WASM
5//! types of the [summary](../../../../doc/sasha-gil/wasm/l0-design.md#summary),
6//! over [`super`], the typed L0. It is still native Rust: step 3 exports it
7//! from a `.wasm`.
8//!
9//! - **A value is an `i64` handle,** an index into a table of `naive`
10//!   values, with the slot's generation in the high 32 bits, so a handle
11//!   used after its `drop` traps even when its slot holds a new value. The four constants are the handles `0` to `3`, which `dup`
12//!   answers as they are and `drop` leaves alone, and every `undefined`,
13//!   `null` or boolean an L0 function answers is one of them. Every other handle is one
14//!   owned reference: `dup` makes a new handle and `drop` frees one.
15//! - **A builder is an `i32` handle,** an index into a table of open
16//!   builders, with a generation the same way.
17//! - **Bytes** cross through [`with_memory`], which stands in for the
18//!   memory the loader makes.
19//! - **Code** is a [`Code`] in a table, [`add_code`], which stands in for
20//!   the shared WASM table. A function L0 makes keeps its code index as the
21//!   item `0` of its NaNVM frame, before the frame the program gave, so one
22//!   Rust [`StaticCode`](crate::vm::StaticCode) runs every code index.
23//! - **A throw traps**
24//!   ([`l0-throw-trap.md`](../../../../doc/sasha-gil/wasm/l0-throw-trap.md)).
25//!   [`throw`] keeps the value and traps: in WASM it reaches `unreachable`,
26//!   and here it panics with [`Thrown`]. A throw from NaNVM's own Rust code
27//!   is an `Err`, turned into a [`throw`] only at the export, in [`call`],
28//!   so nothing is borrowed or half made when it traps. The host reads it
29//!   through [`has_thrown`], [`thrown`] and [`recover`], with [`enter`]
30//!   and [`leave`] around each call, which are not L0
31//!   imports. [`catch`] is a native host's whole sequence.
32//!
33//! A misuse traps as [`super`] does, with the text `l0.<name>: <reason>`,
34//! including a handle that names no value, `not a value`, and bytes out of
35//! the memory, `out of bounds`.
36
37#[cfg(test)]
38mod test;
39
40use std::{
41    cell::RefCell,
42    collections::HashMap,
43    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
44};
45
46use super::{Builder, trap};
47use crate::common::sized_index::SizedIndex;
48use crate::{
49    naive::Naive,
50    vm::{Any, Array, Function, IVm, ToAny, ToArray},
51};
52
53type A = Naive;
54type V = Any<A>;
55
56/// The code of a function: `(param $self i64) (param $args i64) (result i64)`
57/// ([`l0-design.md` §12](../../../../doc/sasha-gil/wasm/l0-design.md#12-functions-a-code-index-a-frame-a-length-and-a-text)).
58/// It borrows both handles and answers an owned one, or calls [`throw`].
59pub type Code = fn(self_: i64, args: i64) -> i64;
60
61/// The constant `undefined`.
62pub const UNDEFINED: i64 = 0;
63/// The constant `null`.
64pub const NULL: i64 = 1;
65/// The constant `false`.
66pub const FALSE: i64 = 2;
67/// The constant `true`.
68pub const TRUE: i64 = 3;
69
70const CONSTANTS: usize = 4;
71
72/// The memory a fresh thread starts with: one WASM page.
73const MEMORY: usize = 0x1_0000;
74
75/// The payload of the panic [`throw`] causes in native code. The value is
76/// kept apart, in the runtime, for [`thrown`].
77#[derive(Debug)]
78pub struct Thrown;
79
80/// A table of handles. A handle is a slot's index in its low `bits` and the
81/// slot's generation above, and freeing a slot moves its generation on, so
82/// a handle freed before refers to nothing, even after its slot is reused.
83/// A slot is retired after its last generation.
84struct Slab<T> {
85    slots: Vec<(u32, Option<T>)>,
86    /// Each slot's birth: the runtime's count of handles made, when the
87    /// slot's value was put.
88    born: Vec<u64>,
89    free: Vec<usize>,
90    bits: u32,
91    generations: u32,
92}
93
94impl<T> Slab<T> {
95    fn new(bits: u32, generation_bits: u32) -> Self {
96        Slab {
97            slots: Vec::new(),
98            born: Vec::new(),
99            free: Vec::new(),
100            bits,
101            generations: u32::MAX >> (32 - generation_bits),
102        }
103    }
104
105    /// The slot a handle names, while it holds a value.
106    fn find(&self, h: u64) -> Option<usize> {
107        let i = (h & ((1 << self.bits) - 1)) as usize;
108        let generation = u32::try_from(h >> self.bits).ok()?;
109        match self.slots.get(i) {
110            Some((g, Some(_))) if *g == generation => Some(i),
111            _ => None,
112        }
113    }
114
115    fn get(&self, h: u64) -> Option<&T> {
116        self.find(h).and_then(|i| self.slots[i].1.as_ref())
117    }
118
119    fn get_mut(&mut self, h: u64) -> Option<&mut T> {
120        self.find(h).and_then(|i| self.slots[i].1.as_mut())
121    }
122
123    fn take(&mut self, h: u64) -> Option<T> {
124        let i = self.find(h)?;
125        let (generation, v) = &mut self.slots[i];
126        // A slot whose generations are spent is retired, never reused, so
127        // no handle ever names two values.
128        if *generation < self.generations {
129            *generation += 1;
130            self.free.push(i);
131        }
132        v.take()
133    }
134
135    fn put(&mut self, name: &str, birth: u64, v: T) -> u64 {
136        let i = match self.free.pop() {
137            Some(i) => i,
138            None => {
139                if (self.slots.len() as u64) >> self.bits != 0 {
140                    trap(name, "too many handles")
141                }
142                self.slots.push((0, None));
143                self.born.push(0);
144                self.slots.len() - 1
145            }
146        };
147        self.slots[i].1 = Some(v);
148        self.born[i] = birth;
149        (u64::from(self.slots[i].0) << self.bits) | i as u64
150    }
151
152    /// Frees every slot whose birth is in `births`.
153    fn free_born(&mut self, births: core::ops::RangeInclusive<u64>) {
154        for i in 0..self.slots.len() {
155            if births.contains(&self.born[i]) {
156                let h = (u64::from(self.slots[i].0) << self.bits) | i as u64;
157                self.take(h);
158            }
159        }
160    }
161
162    fn count(&self) -> usize {
163        self.slots.iter().filter(|(_, v)| v.is_some()).count()
164    }
165}
166
167struct State {
168    values: Slab<V>,
169    builders: Slab<Builder<A>>,
170    memory: Vec<u8>,
171    table: Vec<Code>,
172    texts: HashMap<Vec<u16>, &'static str>,
173    /// The number of handles made so far, values and builders: the birth
174    /// of the next one.
175    births: u64,
176    /// For each call of the host into the program, [`enter`], the births
177    /// before it.
178    marks: Vec<u64>,
179    /// The value of the last [`throw`], and the births before it.
180    thrown: Option<(V, u64)>,
181}
182
183impl State {
184    fn birth(&mut self) -> u64 {
185        self.births += 1;
186        self.births
187    }
188}
189
190impl State {
191    fn new() -> Self {
192        // A value handle is 32 bits of index and 32 of generation. A
193        // builder handle, an `i32`, is 16 and 15, so it is never negative.
194        let mut values = Slab::new(32, 32);
195        for v in [
196            super::undefined(),
197            super::null(),
198            super::from_bool(false),
199            super::from_bool(true),
200        ] {
201            values.put("constant", 0, v);
202        }
203        State {
204            values,
205            builders: Slab::new(16, 15),
206            memory: core::iter::repeat_n(0, MEMORY).collect(),
207            table: Vec::new(),
208            texts: HashMap::new(),
209            births: 0,
210            marks: Vec::new(),
211            thrown: None,
212        }
213    }
214}
215
216thread_local! {
217    static STATE: RefCell<State> = RefCell::new(State::new());
218}
219
220fn with<R>(f: impl FnOnce(&mut State) -> R) -> R {
221    STATE.with_borrow_mut(f)
222}
223
224// Handles. No borrow of the state is held while a typed L0 function or a
225// code runs, so either may call back into the ABI.
226
227fn is_constant(h: i64) -> bool {
228    (0..CONSTANTS as i64).contains(&h)
229}
230
231/// The value of a borrowed handle.
232fn get(name: &str, h: i64) -> V {
233    with(|s| s.values.get(h as u64).cloned()).unwrap_or_else(|| trap(name, "not a value"))
234}
235
236/// The value of a handle the function takes: the handle is freed.
237fn take(name: &str, h: i64) -> V {
238    if is_constant(h) {
239        return get(name, h);
240    }
241    with(|s| s.values.take(h as u64)).unwrap_or_else(|| trap(name, "not a value"))
242}
243
244/// A new owned handle. `undefined`, `null` and the booleans answer their
245/// constants, so they never take a slot.
246fn put(v: V) -> i64 {
247    match super::kind(&v) {
248        super::Kind::Undefined => return UNDEFINED,
249        super::Kind::Null => return NULL,
250        super::Kind::Boolean => return from_bool(super::to_bool(&v) as i32),
251        _ => {}
252    }
253    with(|s| {
254        let birth = s.birth();
255        s.values.put("put", birth, v)
256    }) as i64
257}
258
259fn take_builder(name: &str, b: i32) -> Builder<A> {
260    with(|s| s.builders.take(b as u32 as u64)).unwrap_or_else(|| trap(name, "not a builder"))
261}
262
263fn put_builder(name: &str, b: Builder<A>) -> i32 {
264    with(|s| {
265        let birth = s.birth();
266        s.builders.put(name, birth, b)
267    }) as i32
268}
269
270/// Runs `f` on an open builder, which stays open.
271fn on_builder(name: &str, b: i32, f: impl FnOnce(&mut Builder<A>)) {
272    with(|s| {
273        let builder = s
274            .builders
275            .get_mut(b as u32 as u64)
276            .unwrap_or_else(|| trap(name, "not a builder"));
277        f(builder)
278    })
279}
280
281/// A count, which an `i32` carries: a negative one traps.
282fn count(name: &str, n: i32) -> u32 {
283    u32::try_from(n).unwrap_or_else(|_| trap(name, "not a count"))
284}
285
286// Memory.
287
288/// The host's access to the memory, as a loader writes the program's bytes.
289pub fn with_memory<R>(f: impl FnOnce(&mut Vec<u8>) -> R) -> R {
290    with(|s| f(&mut s.memory))
291}
292
293fn bytes(name: &str, ptr: i32, len: i32, size: usize) -> core::ops::Range<usize> {
294    let start = ptr as u32 as usize;
295    let n = (len as u32 as usize).checked_mul(size);
296    match n.and_then(|n| start.checked_add(n)) {
297        Some(end) if end <= with(|s| s.memory.len()) => start..end,
298        _ => trap(name, "out of bounds"),
299    }
300}
301
302fn load_units(name: &str, ptr: i32, len: i32) -> Vec<u16> {
303    let r = bytes(name, ptr, len, 2);
304    with(|s| {
305        s.memory[r]
306            .as_chunks::<2>()
307            .0
308            .iter()
309            .map(|&c| u16::from_le_bytes(c))
310            .collect()
311    })
312}
313
314fn load_words(name: &str, ptr: i32, len: i32) -> Vec<u64> {
315    let r = bytes(name, ptr, len, 8);
316    with(|s| {
317        s.memory[r]
318            .as_chunks::<8>()
319            .0
320            .iter()
321            .map(|&c| u64::from_le_bytes(c))
322            .collect()
323    })
324}
325
326fn store(name: &str, ptr: i32, len: i32, size: usize, data: &[u8]) {
327    let r = bytes(name, ptr, len, size);
328    with(|s| s.memory[r].copy_from_slice(data))
329}
330
331// Scalars.
332
333/// L0 `kind`.
334pub fn kind(v: i64) -> i32 {
335    super::kind(&get("kind", v)) as i32
336}
337
338/// L0 `to_f64`.
339pub fn to_f64(v: i64) -> f64 {
340    super::to_f64(&get("to_f64", v))
341}
342
343/// L0 `from_f64`.
344pub fn from_f64(x: f64) -> i64 {
345    put(super::from_f64(x))
346}
347
348/// L0 `to_bool`.
349pub fn to_bool(v: i64) -> i32 {
350    super::to_bool(&get("to_bool", v)) as i32
351}
352
353/// L0 `from_bool`: the constant `true` for any `b` but `0`.
354pub fn from_bool(b: i32) -> i64 {
355    if b == 0 { FALSE } else { TRUE }
356}
357
358// Bytes.
359
360/// L0 `make_string`: `len` UTF-16 units at `ptr`.
361pub fn make_string(ptr: i32, len: i32) -> i64 {
362    put(super::make_string(&load_units("make_string", ptr, len)))
363}
364
365/// L0 `read_units`: `len` units of `s` from `start`, written at `ptr`.
366pub fn read_units(s: i64, start: i32, len: i32, ptr: i32) {
367    bytes("read_units", ptr, len, 2);
368    let mut out: Vec<u16> = core::iter::repeat_n(0, len as u32 as usize).collect();
369    super::read_units(&get("read_units", s), start as u32, &mut out);
370    let data: Vec<u8> = out.iter().flat_map(|u| u.to_le_bytes()).collect();
371    store("read_units", ptr, len, 2, &data);
372}
373
374/// L0 `make_bigint`: a sign and `len` `u64` words at `ptr`.
375pub fn make_bigint(sign: i32, ptr: i32, len: i32) -> i64 {
376    put(super::make_bigint(
377        sign,
378        &load_words("make_bigint", ptr, len),
379    ))
380}
381
382/// L0 `sign`.
383pub fn sign(b: i64) -> i32 {
384    super::sign(&get("sign", b))
385}
386
387/// L0 `read_words`: `len` words of `b`'s magnitude from `start`, written
388/// at `ptr`.
389pub fn read_words(b: i64, start: i32, len: i32, ptr: i32) {
390    bytes("read_words", ptr, len, 8);
391    let mut out: Vec<u64> = core::iter::repeat_n(0, len as u32 as usize).collect();
392    super::read_words(&get("read_words", b), start as u32, &mut out);
393    let data: Vec<u8> = out.iter().flat_map(|w| w.to_le_bytes()).collect();
394    store("read_words", ptr, len, 8, &data);
395}
396
397// Strings.
398
399/// L0 `unit`.
400pub fn unit(s: i64, i: i32) -> i32 {
401    super::unit(&get("unit", s), i as u32).into()
402}
403
404/// L0 `from_unit`. A number that is not a UTF-16 unit traps.
405pub fn from_unit(u: i32) -> i64 {
406    let u = u16::try_from(u).unwrap_or_else(|_| trap("from_unit", "not a unit"));
407    put(super::from_unit(u))
408}
409
410/// L0 `concat`.
411pub fn concat(a: i64, b: i64) -> i64 {
412    put(super::concat(&get("concat", a), &get("concat", b)))
413}
414
415/// L0 `substring`.
416pub fn substring(s: i64, start: i32, end: i32) -> i64 {
417    put(super::substring(
418        &get("substring", s),
419        start as u32,
420        end as u32,
421    ))
422}
423
424/// L0 `join`.
425pub fn join(parts: i64, separator: i64) -> i64 {
426    put(super::join(&get("join", parts), &get("join", separator)))
427}
428
429// Containers.
430
431/// L0 `begin_array`.
432pub fn begin_array(len: i32) -> i32 {
433    put_builder("begin_array", super::begin_array(count("begin_array", len)))
434}
435
436/// L0 `begin_object`.
437pub fn begin_object(len: i32) -> i32 {
438    put_builder(
439        "begin_object",
440        super::begin_object(count("begin_object", len)),
441    )
442}
443
444/// L0 `push`. It takes `v`.
445pub fn push(b: i32, v: i64) {
446    let v = take("push", v);
447    on_builder("push", b, |b| b.push(v))
448}
449
450/// L0 `push_property`. It takes `key` and `v`.
451pub fn push_property(b: i32, key: i64, v: i64) {
452    let key = take("push_property", key);
453    let v = take("push_property", v);
454    on_builder("push_property", b, |b| b.push_property(key, v))
455}
456
457/// L0 `seal`. It consumes the builder.
458pub fn seal(b: i32) -> i64 {
459    put(take_builder("seal", b).seal())
460}
461
462/// L0 `length`.
463pub fn length(v: i64) -> i32 {
464    super::length(&get("length", v)) as i32
465}
466
467/// L0 `item`.
468pub fn item(v: i64, i: i32) -> i64 {
469    put(super::item(&get("item", v), i as u32))
470}
471
472/// L0 `key`.
473pub fn key(o: i64, i: i32) -> i64 {
474    put(super::key(&get("key", o), i as u32))
475}
476
477/// L0 `find`.
478pub fn find(o: i64, key: i64) -> i32 {
479    super::find(&get("find", o), &get("find", key))
480}
481
482// Lifetime.
483
484/// L0 `dup`.
485pub fn dup(v: i64) -> i64 {
486    let x = get("dup", v);
487    if is_constant(v) { v } else { put(x) }
488}
489
490/// L0 `drop`. It takes `v`.
491pub fn drop(v: i64) {
492    take("drop", v);
493}
494
495// Functions.
496
497/// The loader's step that puts a code in the table, answering its index.
498pub fn add_code(code: Code) -> i32 {
499    with(|s| {
500        s.table.push(code);
501        (s.table.len() - 1) as i32
502    })
503}
504
505/// The one Rust code of every function L0 makes: it reads the code index
506/// from the frame and runs that code on two handles it frees after.
507fn run(self_: &<A as IVm>::InternalFunction, args: Array<A>) -> Result<V, V> {
508    let index = super::to_f64(&A::frame(self_)[0]) as usize;
509    let code = with(|s| s.table[index]);
510    let handles = Handles([
511        put(Function::<A>::new(self_.clone()).to_any()),
512        put(args.to_any()),
513    ]);
514    Ok(take("call", code(handles.0[0], handles.0[1])))
515}
516
517/// Frees borrowed handles, also when a throw unwinds past them natively.
518struct Handles([i64; 2]);
519
520impl Drop for Handles {
521    fn drop(&mut self) {
522        for h in self.0 {
523            take("call", h);
524        }
525    }
526}
527
528/// A function's text as NaNVM keeps it: one `&'static str` per distinct
529/// text, made once. An unpaired surrogate becomes U+FFFD.
530fn intern(text: &V) -> &'static str {
531    let mut units: Vec<u16> = core::iter::repeat_n(0, super::length(text) as usize).collect();
532    super::read_units(text, 0, &mut units);
533    if let Some(t) = with(|s| s.texts.get(&units).copied()) {
534        return t;
535    }
536    let t: &'static str = std::string::String::from_utf16_lossy(&units).leak();
537    with(|s| s.texts.insert(units, t));
538    t
539}
540
541/// L0 `make_function`. It takes `frame` and `text`, a string or
542/// `undefined`.
543pub fn make_function(code: i32, frame: i64, length: i32, text: i64) -> i64 {
544    if !with(|s| usize::try_from(code).is_ok_and(|i| i < s.table.len())) {
545        trap("make_function", "not a code index")
546    }
547    let length = count("make_function", length);
548    let frame = take("make_function", frame);
549    let text = take("make_function", text);
550    let text = match super::kind(&text) {
551        super::Kind::Undefined => None,
552        super::Kind::String => Some(intern(&text)),
553        _ => trap("make_function", "not a string"),
554    };
555    let frame = super::as_array("make_function", &frame);
556    let items: Vec<V> = core::iter::once(super::from_f64(code as f64))
557        .chain(frame.index_iter())
558        .collect();
559    put(A::static_function(run, length, items.to_array(), text).to_any())
560}
561
562/// L0 `call`. A throw, from the code or from NaNVM's own, becomes
563/// [`throw`] here.
564pub fn call(f: i64, args: i64) -> i64 {
565    let f = get("call", f);
566    let args = get("call", args);
567    match super::call(&f, &args) {
568        Ok(v) => put(v),
569        Err(e) => throw(put(e)),
570    }
571}
572
573/// L0 `frame`: the item `i` of the frame the program gave
574/// `make_function`, read through the running function, `self_`.
575pub fn frame(self_: i64, i: i32) -> i64 {
576    let f = super::as_function("frame", &get("frame", self_));
577    let frame = A::frame(f.internal());
578    let n = frame.length().saturating_sub(1);
579    put(frame[super::index("frame", i as u32, n) + 1].clone())
580}
581
582/// L0 `text`.
583pub fn text(f: i64) -> i64 {
584    put(super::text(&get("text", f)))
585}
586
587/// L0 `throw`: keeps `v`, which it takes, and traps.
588pub fn throw(v: i64) -> ! {
589    let v = take("throw", v);
590    with(|s| s.thrown = Some((v, s.births)));
591    #[cfg(all(target_arch = "wasm32", panic = "abort"))]
592    core::arch::wasm32::unreachable();
593    #[cfg(not(all(target_arch = "wasm32", panic = "abort")))]
594    std::panic::panic_any(Thrown)
595}
596
597// Identity.
598
599/// L0 `same`.
600pub fn same(a: i64, b: i64) -> i32 {
601    super::same(&get("same", a), &get("same", b)) as i32
602}
603
604// The host's side, not L0 imports.
605
606/// `1` when a [`throw`] caused the last trap, else `0`: the trap was a
607/// misuse.
608pub fn has_thrown() -> i32 {
609    with(|s| s.thrown.is_some()) as i32
610}
611
612/// The value the last [`throw`] kept, as an owned handle, or `undefined`.
613pub fn thrown() -> i64 {
614    with(|s| s.thrown.as_ref().map(|(v, _)| v.clone())).map_or(UNDEFINED, put)
615}
616
617/// Marks the start of a call of the host into the program, for
618/// [`recover`]. Calls may nest, as when the host answers an effect.
619pub fn enter() {
620    with(|s| s.marks.push(s.births))
621}
622
623/// Ends the call [`enter`] marked, after it returned.
624pub fn leave() {
625    with(|s| s.marks.pop());
626}
627
628/// Makes the runtime ready to go on after a caught throw, and ends the
629/// call [`enter`] marked. It frees every value handle and builder made
630/// after that mark and before the [`throw`]: what the abandoned frames
631/// held, since a program keeps no handle across a call of the host but its
632/// literals, made before any. Then it clears [`has_thrown`]. A handle that
633/// [`thrown`] made stays. Without a mark it frees nothing.
634pub fn recover() {
635    with(|s| {
636        let mark = s.marks.pop();
637        if let (Some(mark), Some((_, at))) = (mark, &s.thrown) {
638            let births = mark + 1..=*at;
639            s.values.free_born(births.clone());
640            s.builders.free_born(births);
641        }
642        s.thrown = None;
643    })
644}
645
646/// A native host's call into the program: `Ok` with the result, or `Err`
647/// with the thrown value, after [`recover`]. A misuse panics on. It needs
648/// `panic = "unwind"`: with `abort`, every trap ends the process, and a
649/// WASM host catches the trap itself.
650pub fn catch(f: impl FnOnce() -> i64) -> Result<i64, i64> {
651    enter();
652    match catch_unwind(AssertUnwindSafe(f)) {
653        Ok(v) => {
654            leave();
655            Ok(v)
656        }
657        Err(payload) => {
658            if has_thrown() == 0 {
659                resume_unwind(payload)
660            }
661            let e = thrown();
662            recover();
663            Err(e)
664        }
665    }
666}
667
668/// The number of handles in use, besides the constants: for tests of
669/// ownership.
670pub fn live() -> usize {
671    with(|s| s.values.count() - CONSTANTS)
672}