Skip to main content

nanvm_lib/vm/lambda/
mod.rs

1//! The three continuation types of `fjs/edag/README.md`'s Chains section,
2//! one per combination of its two bits — a receiver is live (P), a
3//! short-circuit region is open (O) — that is not a node boundary. A
4//! chain is opened by a node's entry point on [`Any`] (`dot`, `option_dot`,
5//! `option_call` in `vm/any`), continued by one method per step the README
6//! allows in that state, and closed by `end` or `end_call`, which alone
7//! answer the chain's `Result`. A step the README does not allow is a
8//! method that does not exist, so a spelling that disagrees with the
9//! README does not compile.
10//!
11//! None of the three is `Clone`, and every method consumes `self`: a
12//! continuation "cannot be lifted out as a shared node", so a chain is used
13//! once, and `#[must_use]` makes a chain used zero times a warning. What a
14//! terminal answers is an ordinary `Result<Any<A>, Any<A>>`, shareable as
15//! any value is — neither bit is ever the result of an `exp`.
16//!
17//! A step's order is *guard, thunk, operation*: a guarded step looks at the
18//! current value for nullishness first and skips its thunk on it; every
19//! callable validation follows argument evaluation, and a computed key
20//! precedes its property read. A member call resolves its property and
21//! runtime callee before forcing arguments, retaining lookup failures;
22//! resolving a noncallable value does not validate it yet.
23//!
24//! A property step holds its receiver and key, [`Member`], and the read
25//! waits for the exit: `end` reads, and a call exit resolves the callee —
26//! an object's own property or an array's element first, then the
27//! function the runtime value's `dot` answers for the receiver, then the
28//! receiver type's built-in member function ([`method`](method::method))
29//! for a name without a body, then the `TypeError` for calling `undefined`. A call step on a member
30//! takes the runtime value for that, `&runtime`, the parameter every
31//! compiled module receives (`fjs/js/runtime/README.md`, "How NaNVM finds
32//! it"); a step on a plain value needs none. No user function reads
33//! `this`, so `Function::call` takes no receiver and never will: the
34//! receiver is consumed by the built-in and handed to nothing. Which
35//! built-ins the table answers is `nanvm/lib/todo/member-functions.md`.
36
37use crate::vm::{Any, IVm};
38
39pub(crate) mod member;
40mod method;
41mod number;
42mod string;
43
44/// The shared runtime-only pairs and the completeness table `method`'s test
45/// checks it against, generated by `fjs/nanvm/methods` — a `#[path]`, since a
46/// `gen.` name is not a Rust identifier.
47#[path = "gen.methods.rs"]
48mod methods_table;
49
50pub(crate) use member::Member;
51
52/// What a chain's live state can do, whatever it holds: a bare value, the
53/// state after a call step or a `?.()` node, or a [`Member`], the receiver
54/// and key of a property step whose read waits for the exit.
55pub(crate) trait Live<A: IVm>: Sized {
56    /// The chain's current value: the value itself, or the property read.
57    fn value(self) -> Any<A>;
58    /// The guard of a `?.()` step: whether what the call would call is
59    /// nullish, or the throw of deciding, a member's body deciding for its
60    /// receiver, which the runtime value's `dot` answers.
61    fn is_nullish(&self, runtime: &Any<A>) -> Result<bool, Any<A>>;
62    /// Resolve before arguments, retaining lookup failures while postponing
63    /// callable validation. Bare values have no member lookup to perform.
64    fn resolve(&self, runtime: &Any<A>) -> Result<(), Any<A>>;
65    /// The call, its arguments evaluated: `Any::call` of a value, and a
66    /// member's resolution — the receiver's own property, the function
67    /// the runtime value's `dot` answers, the native for a name without a
68    /// body, the `TypeError`.
69    fn call(self, runtime: &Any<A>, args: Any<A>) -> Result<Any<A>, Any<A>>;
70}
71
72impl<A: IVm> Live<A> for Any<A> {
73    fn value(self) -> Any<A> {
74        self
75    }
76    fn is_nullish(&self, _runtime: &Any<A>) -> Result<bool, Any<A>> {
77        Ok(Any::is_nullish(self))
78    }
79    fn resolve(&self, _runtime: &Any<A>) -> Result<(), Any<A>> {
80        Ok(())
81    }
82    fn call(self, _runtime: &Any<A>, args: Any<A>) -> Result<Any<A>, Any<A>> {
83        Any::call(self, args)
84    }
85}
86
87impl<A: IVm> Live<A> for Member<A> {
88    fn value(self) -> Any<A> {
89        self.read()
90    }
91    fn is_nullish(&self, runtime: &Any<A>) -> Result<bool, Any<A>> {
92        Member::is_nullish(self, runtime)
93    }
94    fn resolve(&self, runtime: &Any<A>) -> Result<(), Any<A>> {
95        self.is_nullish(runtime).map(|_| ())
96    }
97    fn call(self, runtime: &Any<A>, args: Any<A>) -> Result<Any<A>, Any<A>> {
98        Member::call(self, runtime, args)
99    }
100}
101
102/// The state of a chain inside an open region: live, skipped by a guard, or
103/// thrown. Skipped and thrown pass through every step; `end` tells them
104/// apart.
105pub(crate) enum Region<A: IVm, T: Live<A>> {
106    Live(T),
107    Skipped,
108    Thrown(Any<A>),
109}
110
111impl<A: IVm, T: Live<A>> From<Result<T, Any<A>>> for Region<A, T> {
112    fn from(r: Result<T, Any<A>>) -> Self {
113        match r {
114            Ok(v) => Region::Live(v),
115            Err(e) => Region::Thrown(e),
116        }
117    }
118}
119
120impl<A: IVm, T: Live<A>> Region<A, T> {
121    /// The region closed: a skipped chain is `undefined`, the value a
122    /// short-circuit answers.
123    fn end(self) -> Result<Any<A>, Any<A>> {
124        match self {
125            Region::Live(v) => Ok(v.value()),
126            Region::Skipped => Ok(Any::undefined()),
127            Region::Thrown(e) => Err(e),
128        }
129    }
130    /// An unguarded step: `op` over a live state, nothing otherwise.
131    fn step<U: Live<A>>(self, op: impl FnOnce(T) -> Result<U, Any<A>>) -> Region<A, U> {
132        match self {
133            Region::Live(v) => op(v).into(),
134            Region::Skipped => Region::Skipped,
135            Region::Thrown(e) => Region::Thrown(e),
136        }
137    }
138    /// `|()`: resolve a member, force the arguments, then call. Callable
139    /// validation follows arguments; lookup failure precedes them.
140    fn call(
141        self,
142        runtime: &Any<A>,
143        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
144    ) -> Region<A, Any<A>> {
145        self.step(|v| {
146            v.resolve(runtime)
147                .and_then(|()| args())
148                .and_then(|a| v.call(runtime, a))
149        })
150    }
151    /// `|.`: the key forced, then the step over the current value as the
152    /// receiver, whose nullish throw therefore comes second.
153    pub(crate) fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> Region<A, Member<A>> {
154        self.step(|v| key().and_then(|k| Member::new(v.value(), k)))
155    }
156    /// A guard: the region skipped where the live state is nullish, thrown
157    /// where deciding throws, and otherwise `step` over it; nothing for a
158    /// region already skipped or thrown. A member decides with the runtime
159    /// value, whose `dot` may answer a callee for its receiver.
160    fn guarded<U: Live<A>>(
161        self,
162        runtime: &Any<A>,
163        step: impl FnOnce(Self) -> Region<A, U>,
164    ) -> Region<A, U> {
165        match self {
166            Region::Live(v) => match v.is_nullish(runtime) {
167                Err(e) => Region::Thrown(e),
168                Ok(true) => Region::Skipped,
169                Ok(false) => step(Region::Live(v)),
170            },
171            Region::Skipped => Region::Skipped,
172            Region::Thrown(e) => Region::Thrown(e),
173        }
174    }
175    /// `|?.()`: a nullish callee skips the rest of the chain, its
176    /// arguments untouched; any other is called.
177    pub(crate) fn option_call(
178        self,
179        runtime: &Any<A>,
180        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
181    ) -> Region<A, Any<A>> {
182        // The guard already performed the preargument resolution.
183        self.guarded(runtime, |live| {
184            live.step(|v| args().and_then(|a| v.call(runtime, a)))
185        })
186    }
187}
188
189impl<A: IVm> Region<A, Any<A>> {
190    /// The `?.` node's own step, on a plain value: a nullish receiver skips
191    /// the key and the rest of the chain; any other takes the step `|.`
192    /// takes. The guard is the value's own nullishness, which asks no
193    /// runtime.
194    pub(crate) fn option_dot(
195        self,
196        key: impl FnOnce() -> Result<Any<A>, Any<A>>,
197    ) -> Region<A, Member<A>> {
198        match self {
199            Region::Live(v) if Any::is_nullish(&v) => Region::Skipped,
200            live => live.dot(key),
201        }
202    }
203    /// `|()` on a plain value, the state after a call step or a `?.()`
204    /// node: `Any::call`, which asks no runtime.
205    fn call_value(self, args: impl FnOnce() -> Result<Any<A>, Any<A>>) -> Region<A, Any<A>> {
206        self.step(|v| args().and_then(|a| Any::call(v, a)))
207    }
208    /// `?.()` on a plain value, the node's own step and a `|?.()` step on
209    /// the state after a call: a nullish callee skips, any other is called.
210    pub(crate) fn option_call_value(
211        self,
212        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
213    ) -> Region<A, Any<A>> {
214        match self {
215            Region::Live(v) if Any::is_nullish(&v) => Region::Skipped,
216            live => live.call_value(args),
217        }
218    }
219}
220
221/// A receiver is live and no region is open: the state after a `.` node,
222/// `a.b`. Its steps are `|()` (`end_call`) and `|?.()` (`option_call`);
223/// `end` is the bare node, the read with its receiver dropped — the one
224/// property read `nanvm-lib` has, `dot(a, key).end()`.
225///
226/// No region, so no skipped state: the interior is the step's own
227/// `Result`, its nullish-receiver throw waiting for a terminal to surface
228/// it — `a.b(...c)` on a nullish `a` throws at the access with `c`
229/// untouched, which is why `end_call` takes its arguments as a thunk.
230#[must_use]
231pub struct PropertyLambda<A: IVm>(pub(crate) Result<Member<A>, Any<A>>);
232
233impl<A: IVm> PropertyLambda<A> {
234    /// No continuation: `a.b`.
235    pub fn end(self) -> Result<Any<A>, Any<A>> {
236        self.0.map(Member::read)
237    }
238    /// `|()`, terminal: `a.b(...args)`, the property called on its
239    /// receiver — an own property, or a built-in member function. No
240    /// region is open, so this is also what a `|!()` would be — "there is
241    /// no bit to clear".
242    pub fn end_call(
243        self,
244        runtime: &Any<A>,
245        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
246    ) -> Result<Any<A>, Any<A>> {
247        // A `Result`-sourced region is never skipped, so `end` is exact.
248        Region::from(self.0).call(runtime, args).end()
249    }
250    /// `|()` of an intrinsic's helper, `slice(s, a, b)`: the member call
251    /// that asks no runtime, the own property else the native else the
252    /// `TypeError`, since no body defines the name
253    /// ([`Member::call_native`]).
254    pub fn end_call_native(
255        self,
256        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
257    ) -> Result<Any<A>, Any<A>> {
258        self.0.and_then(|m| args().and_then(|a| m.call_native(a)))
259    }
260    /// `|?.()`: `a.b?.(...args)`, opening a region.
261    pub fn option_call(
262        self,
263        runtime: &Any<A>,
264        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
265    ) -> OptionLambda<A> {
266        OptionLambda(Region::from(self.0).option_call(runtime, args))
267    }
268}
269
270/// A region is open and no receiver is live: the state after a `?.()` node,
271/// `a?.(...b)`, or a `|()` or `|?.()` step inside a region. Its steps are
272/// `|()` (`call`) and `|.` (`dot`); no `|?.()`, and no `|!()` — there is no
273/// receiver for either to justify.
274#[must_use]
275pub struct OptionLambda<A: IVm>(pub(crate) Region<A, Any<A>>);
276
277impl<A: IVm> OptionLambda<A> {
278    /// No continuation: the region closes, a skipped chain is `undefined`.
279    pub fn end(self) -> Result<Any<A>, Any<A>> {
280        self.0.end()
281    }
282    /// `|()`: call the current value, inside the region.
283    pub fn call(self, args: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionLambda<A> {
284        OptionLambda(self.0.call_value(args))
285    }
286    /// `|.`: a property of the current value, inside the region; the value
287    /// becomes the receiver.
288    pub fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionPropertyLambda<A> {
289        OptionPropertyLambda(self.0.dot(key))
290    }
291}
292
293/// Both bits live: the state after a `?.` node, `a?.b`, or a `|.` step
294/// inside a region. Every step is legal here: `|()` (`call`), `|.` (`dot`),
295/// `|?.()` (`option_call`) and `|!()` (`end_call`).
296#[must_use]
297pub struct OptionPropertyLambda<A: IVm>(pub(crate) Region<A, Member<A>>);
298
299impl<A: IVm> OptionPropertyLambda<A> {
300    /// No continuation: `(a?.b)`, the region closed with nothing after it.
301    pub fn end(self) -> Result<Any<A>, Any<A>> {
302        self.0.end()
303    }
304    /// `|()`: `a?.b(...args)`, the property called on its receiver, skipped
305    /// with the region.
306    pub fn call(
307        self,
308        runtime: &Any<A>,
309        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
310    ) -> OptionLambda<A> {
311        OptionLambda(self.0.call(runtime, args))
312    }
313    /// `|.`: `a?.b.c`, skipped with the region.
314    pub fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionPropertyLambda<A> {
315        OptionPropertyLambda(self.0.dot(key))
316    }
317    /// `|?.()`: `a?.b?.(...args)`, its own guard on the callee.
318    pub fn option_call(
319        self,
320        runtime: &Any<A>,
321        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
322    ) -> OptionLambda<A> {
323        OptionLambda(self.0.option_call(runtime, args))
324    }
325    /// `|!()`, terminal: `(a?.b)(...args)`. The parentheses close the
326    /// region first, so a skipped chain is `undefined` here and the call
327    /// happens regardless — the arguments are evaluated, then `undefined`
328    /// is called and throws. A live chain calls the property on its
329    /// receiver; a thrown chain stays thrown, arguments untouched.
330    pub fn end_call(
331        self,
332        runtime: &Any<A>,
333        args: impl FnOnce() -> Result<Any<A>, Any<A>>,
334    ) -> Result<Any<A>, Any<A>> {
335        match self.0 {
336            Region::Skipped => args().and_then(|a| Any::undefined().call(a)),
337            region => region.call(runtime, args).end(),
338        }
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use crate::vm::test::function::{identity, returns, rt};
345    use crate::{
346        naive::Naive,
347        vm::{Any, ToAny, ToArray, ToObject, error},
348    };
349
350    type A = Naive;
351    type Thunk = Box<dyn FnOnce() -> Result<Any<A>, Any<A>>>;
352
353    /// A thunk that must not be forced.
354    fn boom() -> Result<Any<A>, Any<A>> {
355        Err("boom".into())
356    }
357    /// A fresh arguments array, for a chain whose answer is not the array.
358    fn args() -> Result<Any<A>, Any<A>> {
359        Ok([7.0.to_any()].to_array().to_any())
360    }
361    /// Asserts that `chain`, handed a thunk over a fresh arguments array,
362    /// answers that very array: a call of `identity` reached its callee.
363    /// Arrays compare by identity, so the array is made once and cloned.
364    fn answers_arguments(chain: impl FnOnce(Thunk) -> Result<Any<A>, Any<A>>) {
365        let a = args().unwrap();
366        let thunk = a.clone();
367        assert_eq!(chain(Box::new(|| Ok(thunk))), Ok(a));
368    }
369    /// `{ f: identity, n: 1, u: undefined }`
370    fn object() -> Any<A> {
371        [
372            ("f".into(), identity()),
373            ("n".into(), 1.0.to_any()),
374            ("u".into(), Any::undefined()),
375        ]
376        .to_object()
377        .to_any()
378    }
379    fn key(k: &str) -> impl FnOnce() -> Result<Any<A>, Any<A>> {
380        let k: Any<A> = k.into();
381        || Ok(k)
382    }
383
384    // `PropertyLambda`
385
386    /// `a.b` — `end` is the read, throw included.
387    #[test]
388    fn property_end() {
389        assert_eq!(object().dot("n".into()).end(), Ok(1.0.to_any()));
390        assert_eq!(
391            Any::<A>::undefined().dot("n".into()).end(),
392            Err(error::nullish_to_object())
393        );
394    }
395
396    /// `a.f(...c)`, and `a.b(...c)` on a nullish `a`: the throw waited in
397    /// the lambda and `c` was never evaluated.
398    #[test]
399    fn property_end_call() {
400        answers_arguments(|args| object().dot("f".into()).end_call(&rt(), args));
401        assert_eq!(
402            Any::undefined().dot("f".into()).end_call(&rt(), boom),
403            Err(error::nullish_to_object())
404        );
405    }
406
407    /// A live, non-callable callee: the arguments run before the
408    /// callability check, so theirs is the throw.
409    #[test]
410    fn property_end_call_forces_arguments_before_callability() {
411        assert_eq!(
412            object().dot("n".into()).end_call(&rt(), boom),
413            Err("boom".into())
414        );
415        assert_eq!(
416            object().dot("n".into()).end_call(&rt(), args),
417            Err(error::unexpected_type())
418        );
419    }
420
421    /// `a.b?.(...c)`: guarded on the property, then a region.
422    #[test]
423    fn property_option_call() {
424        answers_arguments(|args| object().dot("f".into()).option_call(&rt(), args).end());
425        assert_eq!(
426            object().dot("u".into()).option_call(&rt(), boom).end(),
427            Ok(Any::undefined())
428        );
429        assert_eq!(
430            object().dot("n".into()).option_call(&rt(), boom).end(),
431            Err("boom".into())
432        );
433        assert_eq!(
434            Any::undefined()
435                .dot("f".into())
436                .option_call(&rt(), boom)
437                .end(),
438            Err(error::nullish_to_object())
439        );
440    }
441
442    // `OptionLambda`
443
444    /// `a?.(...c).length` and `a?.().f(...d)`: steps inside the region on
445    /// a live value.
446    #[test]
447    fn option_steps_live() {
448        assert_eq!(
449            identity().option_call(args).dot(key("length")).end(),
450            Ok(1.0.to_any())
451        );
452        let returns_object: Any<A> = returns(object());
453        answers_arguments(|args| {
454            returns_object
455                .option_call(|| Ok([].to_array().to_any()))
456                .dot(key("f"))
457                .call(&rt(), args)
458                .end()
459        });
460    }
461
462    /// A skipped region passes through every step, thunks untouched, and
463    /// ends as `undefined`.
464    #[test]
465    fn option_steps_skipped() {
466        assert_eq!(
467            Any::undefined()
468                .option_call(boom)
469                .dot(boom)
470                .call(&rt(), boom)
471                .dot(boom)
472                .end(),
473            Ok(Any::undefined())
474        );
475    }
476
477    /// A thrown region passes through every step, thunks untouched, and
478    /// ends with its throw.
479    #[test]
480    fn option_steps_thrown() {
481        assert_eq!(
482            identity()
483                .option_call(boom)
484                .dot(key("x"))
485                .call(&rt(), args)
486                .end(),
487            Err("boom".into())
488        );
489    }
490
491    // `OptionPropertyLambda`
492
493    /// `a?.f(...c)`, `a?.n.x`, `a?.u?.(...c)`, `a?.f?.(...c)`: the three
494    /// non-terminal steps on a live chain.
495    #[test]
496    fn option_property_steps_live() {
497        answers_arguments(|args| object().option_dot(key("f")).call(&rt(), args).end());
498        assert_eq!(
499            object().option_dot(key("n")).dot(key("x")).end(),
500            Ok(Any::undefined())
501        );
502        assert_eq!(
503            object().option_dot(key("u")).option_call(&rt(), boom).end(),
504            Ok(Any::undefined())
505        );
506        answers_arguments(|args| object().option_dot(key("f")).option_call(&rt(), args).end());
507    }
508
509    /// `a?.b.c(...d).e?.(...f)` on a nullish `a`: one region, everything
510    /// skipped.
511    #[test]
512    fn option_property_steps_skipped() {
513        assert_eq!(
514            Any::undefined()
515                .option_dot(boom)
516                .dot(boom)
517                .call(&rt(), boom)
518                .dot(boom)
519                .option_call(&rt(), boom)
520                .end(),
521            Ok(Any::undefined())
522        );
523    }
524
525    /// `|()` and `|.` are unguarded: on a live but nullish current value
526    /// the thunk is forced and the operation throws — `a?.b()` with `a.b`
527    /// undefined throws, it does not answer `undefined`. One case per
528    /// unguarded step, so that turning any of them into a guarded step
529    /// fails a test.
530    #[test]
531    fn unguarded_steps_throw_on_nullish_current_value() {
532        let returns_undefined: Any<A> = returns(Any::undefined());
533        assert_eq!(
534            returns_undefined.clone().option_call(args).call(boom).end(),
535            Err("boom".into())
536        );
537        assert_eq!(
538            returns_undefined.option_call(args).dot(boom).end(),
539            Err("boom".into())
540        );
541        assert_eq!(
542            object().option_dot(key("u")).call(&rt(), boom).end(),
543            Err("boom".into())
544        );
545    }
546
547    /// `|.` on a live but nullish current value throws at the read — after
548    /// the key is forced, as JavaScript evaluates `u[k()]`.
549    #[test]
550    fn option_property_dot_forces_key_before_read() {
551        assert_eq!(
552            object().option_dot(key("u")).dot(boom).end(),
553            Err("boom".into())
554        );
555        assert_eq!(
556            object().option_dot(key("u")).dot(key("x")).end(),
557            Err(error::nullish_to_object())
558        );
559    }
560
561    /// `(a?.f)(...c)` live, and `(a?.b)(...c)` on a nullish `a`: the
562    /// parentheses end the region, so the arguments run and `undefined` is
563    /// called — the case the host engines disagree on, and the
564    /// specification's reading.
565    #[test]
566    fn option_property_end_call() {
567        answers_arguments(|args| object().option_dot(key("f")).end_call(&rt(), args));
568        assert_eq!(
569            Any::undefined().option_dot(boom).end_call(&rt(), boom),
570            Err("boom".into())
571        );
572        assert_eq!(
573            Any::undefined().option_dot(boom).end_call(&rt(), args),
574            Err(error::unexpected_type())
575        );
576    }
577
578    /// A thrown chain stays thrown through `end_call`, arguments untouched.
579    #[test]
580    fn option_property_end_call_thrown() {
581        assert_eq!(
582            object().option_dot(boom).end_call(&rt(), boom),
583            Err("boom".into())
584        );
585    }
586}